@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.
Files changed (35) hide show
  1. package/AGENTS.md +2 -1
  2. package/README.md +27 -0
  3. package/agents/crm.md +3 -0
  4. package/agents/svelte.md +3 -1
  5. package/dist/agreements.js +1 -1
  6. package/dist/chunks/{__smrt-register__-DJEOUcaR.js → __smrt-register__-_uoH8d0A.js} +2 -2
  7. package/dist/chunks/{__smrt-register__-DJEOUcaR.js.map → __smrt-register__-_uoH8d0A.js.map} +1 -1
  8. package/dist/chunks/{crm-DwEz7E2r.js → crm-B8XqwUBg.js} +472 -7
  9. package/dist/chunks/crm-B8XqwUBg.js.map +1 -0
  10. package/dist/commissions.js +1 -1
  11. package/dist/crm/collections/SalesActivityCollection.d.ts +2 -2
  12. package/dist/crm/collections/SalesActivityCollection.d.ts.map +1 -1
  13. package/dist/crm/index.d.ts +2 -0
  14. package/dist/crm/index.d.ts.map +1 -1
  15. package/dist/crm/models/SalesActivity.d.ts +17 -0
  16. package/dist/crm/models/SalesActivity.d.ts.map +1 -1
  17. package/dist/crm/services/LeadWorkflowService.d.ts +164 -0
  18. package/dist/crm/services/LeadWorkflowService.d.ts.map +1 -0
  19. package/dist/crm.js +3 -3
  20. package/dist/index.js +3 -3
  21. package/dist/manifest.json +1671 -1663
  22. package/dist/referrals.js +1 -1
  23. package/dist/smrt-knowledge.json +157 -156
  24. package/dist/svelte/__tests__/types.test.js +30 -1
  25. package/dist/svelte/components/LeadDetail.svelte +512 -0
  26. package/dist/svelte/components/LeadDetail.svelte.d.ts +31 -0
  27. package/dist/svelte/components/LeadDetail.svelte.d.ts.map +1 -0
  28. package/dist/svelte/index.d.ts +5 -3
  29. package/dist/svelte/index.d.ts.map +1 -1
  30. package/dist/svelte/index.js +3 -2
  31. package/dist/svelte/types.d.ts +44 -1
  32. package/dist/svelte/types.d.ts.map +1 -1
  33. package/dist/svelte/types.js +17 -0
  34. package/package.json +6 -6
  35. package/dist/chunks/crm-DwEz7E2r.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"__smrt-register__-DJEOUcaR.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
+ {"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: "due_at ASC"
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: "created_at ASC"
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
- export { Lead as S, SalesActivityCollection as _, LeadCollection as a, PipelineStage as b, DEFAULT_PIPELINE_STAGES as c, PIPELINE_STATUSES as d, SALES_ACTIVITY_KINDS as f, OpportunityCollection as g, PipelineDefinition as h, OpportunityConversion as i, LEAD_STATUSES as l, SALES_REPRESENTATIVE_STATUSES as m, SalesRepresentative as n, PipelineDefinitionCollection as o, SALES_ACTIVITY_SUBJECT_KINDS as p, OpportunityConversionCollection as r, DEFAULT_PIPELINE_KEY as s, SalesRepresentativeCollection as t, OPPORTUNITY_STATUSES as u, SalesActivity as v, Opportunity as x, PipelineStageCollection as y };
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-DwEz7E2r.js.map
1893
+ //# sourceMappingURL=crm-B8XqwUBg.js.map