@happyvertical/smrt-sales 0.40.42 → 0.40.44
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/AGENTS.md +2 -1
- package/README.md +27 -0
- package/agents/crm.md +3 -0
- package/agents/svelte.md +3 -1
- package/dist/agreements.js +1 -1
- package/dist/chunks/{__smrt-register__-DJEOUcaR.js → __smrt-register__-_uoH8d0A.js} +2 -2
- package/dist/chunks/{__smrt-register__-DJEOUcaR.js.map → __smrt-register__-_uoH8d0A.js.map} +1 -1
- package/dist/chunks/{crm-DwEz7E2r.js → crm-B8XqwUBg.js} +472 -7
- package/dist/chunks/crm-B8XqwUBg.js.map +1 -0
- package/dist/commissions.js +1 -1
- package/dist/crm/collections/SalesActivityCollection.d.ts +2 -2
- package/dist/crm/collections/SalesActivityCollection.d.ts.map +1 -1
- package/dist/crm/index.d.ts +2 -0
- package/dist/crm/index.d.ts.map +1 -1
- package/dist/crm/models/SalesActivity.d.ts +17 -0
- package/dist/crm/models/SalesActivity.d.ts.map +1 -1
- package/dist/crm/services/LeadWorkflowService.d.ts +164 -0
- package/dist/crm/services/LeadWorkflowService.d.ts.map +1 -0
- package/dist/crm.js +3 -3
- package/dist/index.js +3 -3
- package/dist/manifest.json +1671 -1663
- package/dist/referrals.js +1 -1
- package/dist/smrt-knowledge.json +157 -156
- package/dist/svelte/__tests__/types.test.js +30 -1
- package/dist/svelte/components/LeadDetail.svelte +512 -0
- package/dist/svelte/components/LeadDetail.svelte.d.ts +31 -0
- package/dist/svelte/components/LeadDetail.svelte.d.ts.map +1 -0
- package/dist/svelte/index.d.ts +5 -3
- package/dist/svelte/index.d.ts.map +1 -1
- package/dist/svelte/index.js +3 -2
- package/dist/svelte/types.d.ts +44 -1
- package/dist/svelte/types.d.ts.map +1 -1
- package/dist/svelte/types.js +17 -0
- package/package.json +6 -6
- package/dist/chunks/crm-DwEz7E2r.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"__smrt-register__-
|
|
1
|
+
{"version":3,"file":"__smrt-register__-_uoH8d0A.js","names":[],"sources":["../../src/__smrt-register__.ts"],"sourcesContent":["import { ObjectRegistry } from '@happyvertical/smrt-core';\n\nObjectRegistry.registerPackageManifest(\n new URL('./manifest.json', import.meta.url),\n);\n"],"mappings":""}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SmrtCollection, SmrtObject, crossPackageRef, field, foreignKey, smrt } from "@happyvertical/smrt-core";
|
|
2
|
-
import { TenantScoped, tenantId } from "@happyvertical/smrt-tenancy";
|
|
2
|
+
import { TenantContextError, TenantScoped, requireTenantId, tenantId } from "@happyvertical/smrt-tenancy";
|
|
3
3
|
//#region src/crm/models/Lead.ts
|
|
4
4
|
var __defProp$6 = Object.defineProperty;
|
|
5
5
|
var __getOwnPropDesc$6 = Object.getOwnPropertyDescriptor;
|
|
@@ -479,6 +479,10 @@ var __decorateClass$3 = (decorators, target, key, kind) => {
|
|
|
479
479
|
if (kind && result) __defProp$3(target, key, result);
|
|
480
480
|
return result;
|
|
481
481
|
};
|
|
482
|
+
var workflowCompletionPermits = /* @__PURE__ */ new WeakSet();
|
|
483
|
+
function permitSalesActivityWorkflowCompletion(activity) {
|
|
484
|
+
workflowCompletionPermits.add(activity);
|
|
485
|
+
}
|
|
482
486
|
var SalesActivity = class extends SmrtObject {
|
|
483
487
|
tenantId = null;
|
|
484
488
|
/** Which model the activity attaches to: `'lead'` or `'opportunity'`. */
|
|
@@ -534,6 +538,40 @@ var SalesActivity = class extends SmrtObject {
|
|
|
534
538
|
setMetadata(metadata) {
|
|
535
539
|
this.metadata = JSON.stringify(metadata);
|
|
536
540
|
}
|
|
541
|
+
/**
|
|
542
|
+
* Keep Lead-task completion monotonic and service-owned even for direct
|
|
543
|
+
* model callers. The shared activity model continues to support existing
|
|
544
|
+
* opportunity and imported-history completion flows; only the Lead
|
|
545
|
+
* follow-up workflow owns its task completion transition.
|
|
546
|
+
*/
|
|
547
|
+
async save() {
|
|
548
|
+
const persisted = await this.resolvePersistedState();
|
|
549
|
+
const priorCompletedAt = persisted?.completedAt;
|
|
550
|
+
const requestedCompletedAt = this.completedAt;
|
|
551
|
+
const isNewCompletion = requestedCompletedAt !== null && (priorCompletedAt === null || priorCompletedAt === void 0);
|
|
552
|
+
if (priorCompletedAt && !sameInstant(priorCompletedAt, requestedCompletedAt)) throw new Error("SalesActivity.completedAt is monotonic and cannot be changed once set.");
|
|
553
|
+
if (persisted?.isLeadTask && !isLeadTask(this)) throw new Error("SalesActivity Lead task identity cannot be changed once persisted.");
|
|
554
|
+
if ((persisted?.isLeadTask === true || isLeadTask(this)) && isNewCompletion && !workflowCompletionPermits.has(this)) throw new Error("SalesActivity.completedAt may only be set through LeadWorkflowService.");
|
|
555
|
+
const result = await super.save();
|
|
556
|
+
workflowCompletionPermits.delete(this);
|
|
557
|
+
return result;
|
|
558
|
+
}
|
|
559
|
+
/** `undefined` means this id has not been persisted yet. */
|
|
560
|
+
async resolvePersistedState() {
|
|
561
|
+
if (!this.id) return void 0;
|
|
562
|
+
try {
|
|
563
|
+
const row = await this.db.get(this.tableName, { id: this.id });
|
|
564
|
+
if (!row) return void 0;
|
|
565
|
+
const value = row.completed_at;
|
|
566
|
+
const completedAt = value === null || value === void 0 ? null : value instanceof Date ? value : new Date(String(value));
|
|
567
|
+
return {
|
|
568
|
+
completedAt: completedAt === null || Number.isFinite(completedAt.getTime()) ? completedAt : null,
|
|
569
|
+
isLeadTask: row.subject_kind === "lead" && row.activity_kind === "task"
|
|
570
|
+
};
|
|
571
|
+
} catch {
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
537
575
|
};
|
|
538
576
|
__decorateClass$3([tenantId({ nullable: true })], SalesActivity.prototype, "tenantId", 2);
|
|
539
577
|
__decorateClass$3([field({ required: true })], SalesActivity.prototype, "subjectId", 2);
|
|
@@ -547,6 +585,12 @@ SalesActivity = __decorateClass$3([TenantScoped({ mode: "optional" }), smrt({
|
|
|
547
585
|
mcp: { include: ["list", "create"] },
|
|
548
586
|
cli: false
|
|
549
587
|
})], SalesActivity);
|
|
588
|
+
function sameInstant(expected, actual) {
|
|
589
|
+
return actual !== null && expected.getTime() === actual.getTime();
|
|
590
|
+
}
|
|
591
|
+
function isLeadTask(activity) {
|
|
592
|
+
return activity.subjectKind === "lead" && activity.activityKind === "task";
|
|
593
|
+
}
|
|
550
594
|
//#endregion
|
|
551
595
|
//#region src/crm/collections/SalesActivityCollection.ts
|
|
552
596
|
var SalesActivityCollection = class extends SmrtCollection {
|
|
@@ -567,9 +611,9 @@ var SalesActivityCollection = class extends SmrtCollection {
|
|
|
567
611
|
});
|
|
568
612
|
}
|
|
569
613
|
/**
|
|
570
|
-
* Open next actions for one subject: activities with a due date
|
|
614
|
+
* Open next actions for one subject: `task` activities with a due date
|
|
571
615
|
* (`dueAt` set) that have not been completed (`completedAt` null),
|
|
572
|
-
* soonest due first.
|
|
616
|
+
* soonest due first with a deterministic id tie-breaker.
|
|
573
617
|
*
|
|
574
618
|
* @param subjectKind - `'lead'` or `'opportunity'`
|
|
575
619
|
* @param subjectId - Subject row id
|
|
@@ -579,10 +623,11 @@ var SalesActivityCollection = class extends SmrtCollection {
|
|
|
579
623
|
where: {
|
|
580
624
|
subjectKind,
|
|
581
625
|
subjectId,
|
|
626
|
+
activityKind: "task",
|
|
582
627
|
"dueAt !=": null,
|
|
583
628
|
completedAt: null
|
|
584
629
|
},
|
|
585
|
-
orderBy: "
|
|
630
|
+
orderBy: ["dueAt ASC", "id ASC"]
|
|
586
631
|
});
|
|
587
632
|
}
|
|
588
633
|
};
|
|
@@ -1188,7 +1233,7 @@ var LeadCollection = class extends SmrtCollection {
|
|
|
1188
1233
|
subjectKind: "lead",
|
|
1189
1234
|
"subjectId in": Array.from(visited)
|
|
1190
1235
|
},
|
|
1191
|
-
orderBy: "
|
|
1236
|
+
orderBy: ["createdAt ASC", "id ASC"]
|
|
1192
1237
|
});
|
|
1193
1238
|
}
|
|
1194
1239
|
};
|
|
@@ -1423,6 +1468,426 @@ var SalesRepresentativeCollection = class extends SmrtCollection {
|
|
|
1423
1468
|
}
|
|
1424
1469
|
};
|
|
1425
1470
|
//#endregion
|
|
1426
|
-
|
|
1471
|
+
//#region src/crm/services/LeadWorkflowService.ts
|
|
1472
|
+
var LEAD_HUMAN_ACTIVITY_KINDS = [
|
|
1473
|
+
"note",
|
|
1474
|
+
"call",
|
|
1475
|
+
"email",
|
|
1476
|
+
"meeting"
|
|
1477
|
+
];
|
|
1478
|
+
var MAX_LEAD_WORKFLOW_TEXT_LENGTH = 1e3;
|
|
1479
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
|
|
1480
|
+
function projectLeadWorkQueue(input) {
|
|
1481
|
+
const terminal = input.status === "qualified" || input.status === "merged";
|
|
1482
|
+
const reopenable = input.status === "disqualified";
|
|
1483
|
+
const ownerRepId = input.ownerRepId?.trim() ?? "";
|
|
1484
|
+
const dueAt = input.nextAction?.dueAt ?? null;
|
|
1485
|
+
const dueTime = dueAt instanceof Date ? dueAt.getTime() : NaN;
|
|
1486
|
+
const now = input.now ?? /* @__PURE__ */ new Date();
|
|
1487
|
+
const nowTime = now.getTime();
|
|
1488
|
+
if (terminal) return queueProjection("terminal");
|
|
1489
|
+
if (reopenable) return queueProjection("reopenable");
|
|
1490
|
+
if (!ownerRepId) return queueProjection("unassigned");
|
|
1491
|
+
if (!Number.isFinite(dueTime)) return queueProjection("no_next_action");
|
|
1492
|
+
if (dueTime < nowTime) return queueProjection("overdue");
|
|
1493
|
+
return queueProjection(calendarDayKey(new Date(dueTime), input.timeZone) === calendarDayKey(now, input.timeZone) ? "due_today" : "upcoming");
|
|
1494
|
+
}
|
|
1495
|
+
var LeadWorkflowValidationError = class extends Error {
|
|
1496
|
+
constructor(reason, message) {
|
|
1497
|
+
super(message);
|
|
1498
|
+
this.reason = reason;
|
|
1499
|
+
this.name = "LeadWorkflowValidationError";
|
|
1500
|
+
}
|
|
1501
|
+
reason;
|
|
1502
|
+
code = "LEAD_WORKFLOW_VALIDATION_ERROR";
|
|
1503
|
+
};
|
|
1504
|
+
var singleConnectionMutationTails = /* @__PURE__ */ new WeakMap();
|
|
1505
|
+
var LeadWorkflowService = class LeadWorkflowService {
|
|
1506
|
+
constructor(deps) {
|
|
1507
|
+
this.deps = deps;
|
|
1508
|
+
}
|
|
1509
|
+
deps;
|
|
1510
|
+
static async create(options = {}) {
|
|
1511
|
+
return new LeadWorkflowService({
|
|
1512
|
+
leads: await LeadCollection.create(options),
|
|
1513
|
+
activities: await SalesActivityCollection.create(options),
|
|
1514
|
+
representatives: await SalesRepresentativeCollection.create(options)
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
/** Assign or reassign an active representative, with one audit row per change. */
|
|
1518
|
+
async assignLead(input) {
|
|
1519
|
+
const actorProfileId = this.requireIdentifier(input.actorProfileId, "actorProfileId");
|
|
1520
|
+
const ownerRepId = this.requireIdentifier(input.ownerRepId, "ownerRepId");
|
|
1521
|
+
return await this.runMutation(async (deps, tenantId) => {
|
|
1522
|
+
const lead = await this.lockLead(deps, input.leadId, tenantId);
|
|
1523
|
+
this.assertActiveFollowUpLead(lead);
|
|
1524
|
+
const representative = await deps.representatives.get({ id: ownerRepId }, { cache: false });
|
|
1525
|
+
if (!representative) throw this.refusal("representative_unavailable", "Representative is unavailable in the active tenant");
|
|
1526
|
+
if (!representative.isActive()) throw this.refusal("representative_inactive", "Representative is not active");
|
|
1527
|
+
if (lead.ownerRepId === ownerRepId) return {
|
|
1528
|
+
lead,
|
|
1529
|
+
changed: false
|
|
1530
|
+
};
|
|
1531
|
+
const priorOwnerRepId = lead.ownerRepId;
|
|
1532
|
+
lead.ownerRepId = ownerRepId;
|
|
1533
|
+
await lead.save();
|
|
1534
|
+
await this.appendAudit(deps.activities, lead, {
|
|
1535
|
+
actorProfileId,
|
|
1536
|
+
activityKind: "assignment",
|
|
1537
|
+
summary: priorOwnerRepId ? "Reassigned lead owner" : "Assigned lead owner",
|
|
1538
|
+
metadata: {
|
|
1539
|
+
fromOwnerRepId: priorOwnerRepId || null,
|
|
1540
|
+
toOwnerRepId: ownerRepId,
|
|
1541
|
+
occurredAt: this.now(input.now).toISOString()
|
|
1542
|
+
}
|
|
1543
|
+
});
|
|
1544
|
+
return {
|
|
1545
|
+
lead,
|
|
1546
|
+
changed: true
|
|
1547
|
+
};
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
/** Start a new Lead or reopen a disqualified Lead for active follow-up. */
|
|
1551
|
+
async startWorking(input) {
|
|
1552
|
+
const actorProfileId = this.requireIdentifier(input.actorProfileId, "actorProfileId");
|
|
1553
|
+
return await this.runMutation(async (deps, tenantId) => {
|
|
1554
|
+
const lead = await this.lockLead(deps, input.leadId, tenantId);
|
|
1555
|
+
if (lead.status !== "new" && lead.status !== "disqualified") throw this.refusal("invalid_transition", `Lead cannot start working from '${lead.status}'`);
|
|
1556
|
+
const from = lead.status;
|
|
1557
|
+
lead.status = "working";
|
|
1558
|
+
await lead.save();
|
|
1559
|
+
await this.appendAudit(deps.activities, lead, {
|
|
1560
|
+
actorProfileId,
|
|
1561
|
+
activityKind: "status_change",
|
|
1562
|
+
summary: from === "disqualified" ? "Reopened lead follow-up" : "Started lead follow-up",
|
|
1563
|
+
metadata: {
|
|
1564
|
+
from,
|
|
1565
|
+
to: "working",
|
|
1566
|
+
occurredAt: this.now(input.now).toISOString()
|
|
1567
|
+
}
|
|
1568
|
+
});
|
|
1569
|
+
return lead;
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1572
|
+
/** Disqualify a new or working Lead with a required bounded rationale. */
|
|
1573
|
+
async disqualifyLead(input) {
|
|
1574
|
+
const actorProfileId = this.requireIdentifier(input.actorProfileId, "actorProfileId");
|
|
1575
|
+
const reason = this.requireBoundedText(input.reason, "reason");
|
|
1576
|
+
return await this.runMutation(async (deps, tenantId) => {
|
|
1577
|
+
const lead = await this.lockLead(deps, input.leadId, tenantId);
|
|
1578
|
+
if (lead.status !== "new" && lead.status !== "working") throw this.refusal("invalid_transition", `Lead cannot be disqualified from '${lead.status}'`);
|
|
1579
|
+
const from = lead.status;
|
|
1580
|
+
lead.status = "disqualified";
|
|
1581
|
+
await lead.save();
|
|
1582
|
+
await this.appendAudit(deps.activities, lead, {
|
|
1583
|
+
actorProfileId,
|
|
1584
|
+
activityKind: "status_change",
|
|
1585
|
+
summary: "Disqualified lead",
|
|
1586
|
+
metadata: {
|
|
1587
|
+
from,
|
|
1588
|
+
to: "disqualified",
|
|
1589
|
+
reason,
|
|
1590
|
+
occurredAt: this.now(input.now).toISOString()
|
|
1591
|
+
}
|
|
1592
|
+
});
|
|
1593
|
+
return lead;
|
|
1594
|
+
});
|
|
1595
|
+
}
|
|
1596
|
+
/** Append one immutable human note, call, email, or meeting to an active Lead. */
|
|
1597
|
+
async recordActivity(input) {
|
|
1598
|
+
const actorProfileId = this.requireIdentifier(input.actorProfileId, "actorProfileId");
|
|
1599
|
+
const summary = this.requireBoundedText(input.summary, "summary");
|
|
1600
|
+
if (!LEAD_HUMAN_ACTIVITY_KINDS.includes(input.activityKind)) throw this.refusal("invalid_activity_kind", "Activity kind is not a human follow-up activity");
|
|
1601
|
+
const metadata = this.canonicalMetadata(input.metadata);
|
|
1602
|
+
return await this.runMutation(async (deps, tenantId) => {
|
|
1603
|
+
const lead = await this.lockLead(deps, input.leadId, tenantId);
|
|
1604
|
+
this.assertActiveFollowUpLead(lead);
|
|
1605
|
+
return await deps.activities.create({
|
|
1606
|
+
tenantId,
|
|
1607
|
+
subjectKind: "lead",
|
|
1608
|
+
subjectId: this.requireLeadId(lead),
|
|
1609
|
+
activityKind: input.activityKind,
|
|
1610
|
+
summary,
|
|
1611
|
+
actorProfileId,
|
|
1612
|
+
metadata
|
|
1613
|
+
});
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
/** Schedule one open `task` activity for an active Lead. */
|
|
1617
|
+
async scheduleNextAction(input) {
|
|
1618
|
+
const actorProfileId = this.requireIdentifier(input.actorProfileId, "actorProfileId");
|
|
1619
|
+
const summary = this.requireBoundedText(input.summary, "summary");
|
|
1620
|
+
const dueAt = this.requireValidDate(input.dueAt, "dueAt");
|
|
1621
|
+
const metadata = this.canonicalMetadata(input.metadata);
|
|
1622
|
+
return await this.runMutation(async (deps, tenantId) => {
|
|
1623
|
+
const lead = await this.lockLead(deps, input.leadId, tenantId);
|
|
1624
|
+
this.assertActiveFollowUpLead(lead);
|
|
1625
|
+
return await deps.activities.create({
|
|
1626
|
+
tenantId,
|
|
1627
|
+
subjectKind: "lead",
|
|
1628
|
+
subjectId: this.requireLeadId(lead),
|
|
1629
|
+
activityKind: "task",
|
|
1630
|
+
summary,
|
|
1631
|
+
dueAt,
|
|
1632
|
+
actorProfileId,
|
|
1633
|
+
metadata
|
|
1634
|
+
});
|
|
1635
|
+
});
|
|
1636
|
+
}
|
|
1637
|
+
/**
|
|
1638
|
+
* Complete one open lead task exactly once. The task is locked with its Lead
|
|
1639
|
+
* so concurrent callers serialize; a compatible retry returns the completed
|
|
1640
|
+
* task without adding a second immutable completion event.
|
|
1641
|
+
*/
|
|
1642
|
+
async completeNextAction(input) {
|
|
1643
|
+
const actorProfileId = this.requireIdentifier(input.actorProfileId, "actorProfileId");
|
|
1644
|
+
return await this.runMutation(async (deps, tenantId) => {
|
|
1645
|
+
const lead = await this.lockLead(deps, input.leadId, tenantId);
|
|
1646
|
+
this.assertActiveFollowUpLead(lead);
|
|
1647
|
+
const task = await this.lockLeadTask(deps, input.taskId, this.requireLeadId(lead), tenantId);
|
|
1648
|
+
if (task.activityKind !== "task" || !task.dueAt) throw this.refusal("task_unavailable", "Task is unavailable for this lead");
|
|
1649
|
+
if (task.completedAt) {
|
|
1650
|
+
if ((await this.findTaskCompletionAudit(deps.activities, this.requireLeadId(lead), this.requireActivityId(task)))?.actorProfileId === actorProfileId) return {
|
|
1651
|
+
task,
|
|
1652
|
+
completed: false
|
|
1653
|
+
};
|
|
1654
|
+
throw this.refusal("completion_replay_conflict", "Task was already completed by another actor or without compatible audit evidence");
|
|
1655
|
+
}
|
|
1656
|
+
const completedAt = this.now(input.now);
|
|
1657
|
+
task.completedAt = completedAt;
|
|
1658
|
+
permitSalesActivityWorkflowCompletion(task);
|
|
1659
|
+
await task.save();
|
|
1660
|
+
await this.appendAudit(deps.activities, lead, {
|
|
1661
|
+
actorProfileId,
|
|
1662
|
+
activityKind: "task_completion",
|
|
1663
|
+
summary: `Completed next action: ${task.summary}`,
|
|
1664
|
+
metadata: {
|
|
1665
|
+
taskId: this.requireActivityId(task),
|
|
1666
|
+
completedAt: completedAt.toISOString()
|
|
1667
|
+
}
|
|
1668
|
+
});
|
|
1669
|
+
return {
|
|
1670
|
+
task,
|
|
1671
|
+
completed: true
|
|
1672
|
+
};
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1675
|
+
/** Return a deterministic chronological activity trail across merged Lead history. */
|
|
1676
|
+
async getLeadTimeline(leadId) {
|
|
1677
|
+
const tenantId = this.requireActiveTenant();
|
|
1678
|
+
const lead = await this.readLead(this.deps.leads, leadId, tenantId);
|
|
1679
|
+
return await this.deps.leads.activitiesIncludingMerged(this.requireLeadId(lead));
|
|
1680
|
+
}
|
|
1681
|
+
/** Return the Lead, its visible owner, earliest open task, and queue projection. */
|
|
1682
|
+
async getLeadWorkState(input) {
|
|
1683
|
+
const tenantId = this.requireActiveTenant();
|
|
1684
|
+
const lead = await this.readLead(this.deps.leads, input.leadId, tenantId);
|
|
1685
|
+
const leadId = this.requireLeadId(lead);
|
|
1686
|
+
const [owner, openTasks] = await Promise.all([lead.ownerRepId ? this.deps.representatives.get({ id: lead.ownerRepId }, { cache: false }) : Promise.resolve(null), this.deps.activities.findOpenTasks("lead", leadId)]);
|
|
1687
|
+
const earliestOpenTask = openTasks[0] ?? null;
|
|
1688
|
+
return {
|
|
1689
|
+
lead,
|
|
1690
|
+
owner,
|
|
1691
|
+
earliestOpenTask,
|
|
1692
|
+
queue: projectLeadWorkQueue({
|
|
1693
|
+
status: lead.status,
|
|
1694
|
+
ownerRepId: lead.ownerRepId,
|
|
1695
|
+
nextAction: earliestOpenTask,
|
|
1696
|
+
now: input.now,
|
|
1697
|
+
timeZone: input.timeZone
|
|
1698
|
+
})
|
|
1699
|
+
};
|
|
1700
|
+
}
|
|
1701
|
+
async runMutation(fn) {
|
|
1702
|
+
const tenantId = this.requireActiveTenant();
|
|
1703
|
+
const db = this.deps.leads.db;
|
|
1704
|
+
if (typeof db.transaction !== "function") throw this.refusal("transaction_unavailable", "Lead workflow mutations require a transaction-capable database adapter");
|
|
1705
|
+
const transaction = db.transaction.bind(db);
|
|
1706
|
+
const supportsRowLocks = this.supportsRowLocks(db);
|
|
1707
|
+
const runTransaction = async () => await transaction(async (tx) => fn({
|
|
1708
|
+
leads: await LeadCollection.create({
|
|
1709
|
+
db: tx,
|
|
1710
|
+
_reuseInitializedDb: true,
|
|
1711
|
+
_deferRuntimeInitialization: true
|
|
1712
|
+
}),
|
|
1713
|
+
activities: await SalesActivityCollection.create({
|
|
1714
|
+
db: tx,
|
|
1715
|
+
_reuseInitializedDb: true,
|
|
1716
|
+
_deferRuntimeInitialization: true
|
|
1717
|
+
}),
|
|
1718
|
+
representatives: await SalesRepresentativeCollection.create({
|
|
1719
|
+
db: tx,
|
|
1720
|
+
_reuseInitializedDb: true,
|
|
1721
|
+
_deferRuntimeInitialization: true
|
|
1722
|
+
}),
|
|
1723
|
+
supportsRowLocks
|
|
1724
|
+
}, tenantId));
|
|
1725
|
+
if (supportsRowLocks) return await runTransaction();
|
|
1726
|
+
const turn = (singleConnectionMutationTails.get(db) ?? Promise.resolve()).then(runTransaction, runTransaction);
|
|
1727
|
+
singleConnectionMutationTails.set(db, turn.then(() => void 0, () => void 0));
|
|
1728
|
+
return await turn;
|
|
1729
|
+
}
|
|
1730
|
+
/** PostgreSQL gets a row lock; other adapters are serialized by runMutation. */
|
|
1731
|
+
async lockLead(deps, leadId, tenantId) {
|
|
1732
|
+
if (!UUID_RE.test(leadId)) throw this.refusal("lead_unavailable", "Lead is unavailable in the active tenant");
|
|
1733
|
+
if (deps.supportsRowLocks) {
|
|
1734
|
+
const lead = (await deps.leads.query(`SELECT * FROM ${deps.leads.tableName}
|
|
1735
|
+
WHERE id = $1 AND tenant_id = $2
|
|
1736
|
+
FOR UPDATE`, [leadId, tenantId], { allowRawOnTenantScoped: true }))[0];
|
|
1737
|
+
if (lead && lead.tenantId === tenantId) return lead;
|
|
1738
|
+
throw this.refusal("lead_unavailable", "Lead is unavailable in the active tenant");
|
|
1739
|
+
}
|
|
1740
|
+
return await this.readLead(deps.leads, leadId, tenantId);
|
|
1741
|
+
}
|
|
1742
|
+
async lockLeadTask(deps, taskId, leadId, tenantId) {
|
|
1743
|
+
if (!UUID_RE.test(taskId)) throw this.refusal("task_unavailable", "Task is unavailable for this lead");
|
|
1744
|
+
if (deps.supportsRowLocks) {
|
|
1745
|
+
const task2 = (await deps.activities.query(`SELECT * FROM ${deps.activities.tableName}
|
|
1746
|
+
WHERE id = $1
|
|
1747
|
+
AND subject_kind = 'lead'
|
|
1748
|
+
AND subject_id = $2
|
|
1749
|
+
AND tenant_id = $3
|
|
1750
|
+
FOR UPDATE`, [
|
|
1751
|
+
taskId,
|
|
1752
|
+
leadId,
|
|
1753
|
+
tenantId
|
|
1754
|
+
], { allowRawOnTenantScoped: true }))[0];
|
|
1755
|
+
if (task2 && task2.tenantId === tenantId) return task2;
|
|
1756
|
+
throw this.refusal("task_unavailable", "Task is unavailable for this lead");
|
|
1757
|
+
}
|
|
1758
|
+
const task = await deps.activities.get({ id: taskId }, { cache: false });
|
|
1759
|
+
if (!task || task.tenantId !== tenantId || task.subjectKind !== "lead" || task.subjectId !== leadId) throw this.refusal("task_unavailable", "Task is unavailable for this lead");
|
|
1760
|
+
return task;
|
|
1761
|
+
}
|
|
1762
|
+
async readLead(leads, leadId, tenantId) {
|
|
1763
|
+
if (!UUID_RE.test(leadId)) throw this.refusal("lead_unavailable", "Lead is unavailable in the active tenant");
|
|
1764
|
+
const lead = await leads.get({ id: leadId }, { cache: false });
|
|
1765
|
+
if (!lead || lead.tenantId !== tenantId) throw this.refusal("lead_unavailable", "Lead is unavailable in the active tenant");
|
|
1766
|
+
return lead;
|
|
1767
|
+
}
|
|
1768
|
+
assertActiveFollowUpLead(lead) {
|
|
1769
|
+
if (lead.status !== "new" && lead.status !== "working") throw this.refusal("lead_not_actionable", `Lead is not actionable for ordinary follow-up while '${lead.status}'`);
|
|
1770
|
+
}
|
|
1771
|
+
async appendAudit(activities, lead, input) {
|
|
1772
|
+
return await activities.create({
|
|
1773
|
+
tenantId: lead.tenantId,
|
|
1774
|
+
subjectKind: "lead",
|
|
1775
|
+
subjectId: this.requireLeadId(lead),
|
|
1776
|
+
activityKind: input.activityKind,
|
|
1777
|
+
summary: input.summary,
|
|
1778
|
+
actorProfileId: input.actorProfileId,
|
|
1779
|
+
metadata: JSON.stringify(input.metadata)
|
|
1780
|
+
});
|
|
1781
|
+
}
|
|
1782
|
+
async findTaskCompletionAudit(activities, leadId, taskId) {
|
|
1783
|
+
return (await activities.findBySubject("lead", leadId)).find((candidate) => candidate.activityKind === "task_completion" && candidate.getMetadata().taskId === taskId) ?? null;
|
|
1784
|
+
}
|
|
1785
|
+
requireActiveTenant() {
|
|
1786
|
+
try {
|
|
1787
|
+
return requireTenantId();
|
|
1788
|
+
} catch (error) {
|
|
1789
|
+
if (!(error instanceof TenantContextError)) throw error;
|
|
1790
|
+
throw this.refusal("tenant_context_required", "Lead workflow operations require an active tenant context");
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
canonicalMetadata(metadata) {
|
|
1794
|
+
const value = metadata ?? {};
|
|
1795
|
+
if (!isPlainJsonObject(value) || !isJsonValue(value, /* @__PURE__ */ new Set())) throw this.refusal("invalid_metadata", "Activity metadata must be a plain JSON object with JSON values");
|
|
1796
|
+
return JSON.stringify(value);
|
|
1797
|
+
}
|
|
1798
|
+
requireBoundedText(value, field) {
|
|
1799
|
+
const normalized = typeof value === "string" ? value.trim() : "";
|
|
1800
|
+
if (!normalized) throw this.refusal(field === "summary" ? "summary_required" : "reason_required", `Lead workflow ${field} is required`);
|
|
1801
|
+
if (normalized.length > 1e3) throw this.refusal(field === "summary" ? "summary_too_long" : "reason_too_long", `Lead workflow ${field} must be at most ${MAX_LEAD_WORKFLOW_TEXT_LENGTH} characters`);
|
|
1802
|
+
return normalized;
|
|
1803
|
+
}
|
|
1804
|
+
requireIdentifier(value, field) {
|
|
1805
|
+
const normalized = typeof value === "string" ? value.trim() : "";
|
|
1806
|
+
if (!normalized || (field === "ownerRepId" || field === "actorProfileId") && !UUID_RE.test(normalized)) throw this.refusal(field === "ownerRepId" ? "representative_unavailable" : "invalid_metadata", `Lead workflow ${field} must be a UUID`);
|
|
1807
|
+
return normalized;
|
|
1808
|
+
}
|
|
1809
|
+
requireValidDate(value, field) {
|
|
1810
|
+
if (!(value instanceof Date) || !Number.isFinite(value.getTime())) throw this.refusal("invalid_due_at", `Lead workflow ${field} must be a valid date`);
|
|
1811
|
+
return value;
|
|
1812
|
+
}
|
|
1813
|
+
requireLeadId(lead) {
|
|
1814
|
+
if (!lead.id) throw this.refusal("lead_unavailable", "Lead is unavailable in the active tenant");
|
|
1815
|
+
return lead.id;
|
|
1816
|
+
}
|
|
1817
|
+
requireActivityId(activity) {
|
|
1818
|
+
if (!activity.id) throw this.refusal("task_unavailable", "Task is unavailable for this lead");
|
|
1819
|
+
return activity.id;
|
|
1820
|
+
}
|
|
1821
|
+
now(value) {
|
|
1822
|
+
return value ?? /* @__PURE__ */ new Date();
|
|
1823
|
+
}
|
|
1824
|
+
supportsRowLocks(db) {
|
|
1825
|
+
return typeof db.acquireSession === "function";
|
|
1826
|
+
}
|
|
1827
|
+
refusal(reason, message) {
|
|
1828
|
+
return new LeadWorkflowValidationError(reason, message);
|
|
1829
|
+
}
|
|
1830
|
+
};
|
|
1831
|
+
function queueProjection(state) {
|
|
1832
|
+
return {
|
|
1833
|
+
state,
|
|
1834
|
+
isActionable: state === "unassigned" || state === "no_next_action" || state === "overdue" || state === "due_today" || state === "upcoming",
|
|
1835
|
+
isUnassigned: state === "unassigned",
|
|
1836
|
+
hasNoNextAction: state === "no_next_action",
|
|
1837
|
+
isOverdue: state === "overdue",
|
|
1838
|
+
isDueToday: state === "due_today",
|
|
1839
|
+
isUpcoming: state === "upcoming",
|
|
1840
|
+
isTerminal: state === "terminal",
|
|
1841
|
+
isReopenable: state === "reopenable"
|
|
1842
|
+
};
|
|
1843
|
+
}
|
|
1844
|
+
function calendarDayKey(date, timeZone) {
|
|
1845
|
+
const options = {
|
|
1846
|
+
year: "numeric",
|
|
1847
|
+
month: "2-digit",
|
|
1848
|
+
day: "2-digit"
|
|
1849
|
+
};
|
|
1850
|
+
let formatter;
|
|
1851
|
+
try {
|
|
1852
|
+
formatter = new Intl.DateTimeFormat("en-US", {
|
|
1853
|
+
...options,
|
|
1854
|
+
timeZone
|
|
1855
|
+
});
|
|
1856
|
+
} catch {
|
|
1857
|
+
formatter = new Intl.DateTimeFormat("en-US", options);
|
|
1858
|
+
}
|
|
1859
|
+
const parts = formatter.formatToParts(date);
|
|
1860
|
+
const get = (type) => parts.find((part) => part.type === type)?.value ?? "";
|
|
1861
|
+
return `${get("year")}-${get("month")}-${get("day")}`;
|
|
1862
|
+
}
|
|
1863
|
+
function isPlainJsonObject(value) {
|
|
1864
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1865
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1866
|
+
return prototype === Object.prototype || prototype === null;
|
|
1867
|
+
}
|
|
1868
|
+
function isJsonValue(value, ancestors) {
|
|
1869
|
+
if (value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)) return true;
|
|
1870
|
+
if (Array.isArray(value)) {
|
|
1871
|
+
if (ancestors.has(value)) return false;
|
|
1872
|
+
ancestors.add(value);
|
|
1873
|
+
try {
|
|
1874
|
+
return value.every((item) => isJsonValue(item, ancestors));
|
|
1875
|
+
} finally {
|
|
1876
|
+
ancestors.delete(value);
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
if (isPlainJsonObject(value)) {
|
|
1880
|
+
if (ancestors.has(value)) return false;
|
|
1881
|
+
ancestors.add(value);
|
|
1882
|
+
try {
|
|
1883
|
+
return Object.values(value).every((item) => isJsonValue(item, ancestors));
|
|
1884
|
+
} finally {
|
|
1885
|
+
ancestors.delete(value);
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
return false;
|
|
1889
|
+
}
|
|
1890
|
+
//#endregion
|
|
1891
|
+
export { SalesActivity as C, Lead as D, Opportunity as E, SalesActivityCollection as S, PipelineStage as T, SALES_ACTIVITY_KINDS as _, projectLeadWorkQueue as a, PipelineDefinition as b, OpportunityConversionCollection as c, PipelineDefinitionCollection as d, DEFAULT_PIPELINE_KEY as f, PIPELINE_STATUSES as g, OPPORTUNITY_STATUSES as h, MAX_LEAD_WORKFLOW_TEXT_LENGTH as i, OpportunityConversion as l, LEAD_STATUSES as m, LeadWorkflowService as n, SalesRepresentativeCollection as o, DEFAULT_PIPELINE_STAGES as p, LeadWorkflowValidationError as r, SalesRepresentative as s, LEAD_HUMAN_ACTIVITY_KINDS as t, LeadCollection as u, SALES_ACTIVITY_SUBJECT_KINDS as v, PipelineStageCollection as w, OpportunityCollection as x, SALES_REPRESENTATIVE_STATUSES as y };
|
|
1427
1892
|
|
|
1428
|
-
//# sourceMappingURL=crm-
|
|
1893
|
+
//# sourceMappingURL=crm-B8XqwUBg.js.map
|