@classytic/crm 0.5.1 → 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
@@ -1,10 +1,12 @@
1
1
  import { t as __exportAll } from "./chunk.js";
2
2
  import { Schema } from "mongoose";
3
- import { Repository, aggregateHelpersPlugin, isDuplicateKeyError, methodRegistryPlugin, multiTenantPlugin } from "@classytic/mongokit";
3
+ import { Repository, aggregateHelpersPlugin, isDuplicateKeyError, methodRegistryPlugin, multiTenantPlugin, withTransaction } from "@classytic/mongokit";
4
4
  import { createEvent as createEvent$1, matchEventPattern } from "@classytic/primitives/events";
5
- import { assertAndClaim, defineStateMachine } from "@classytic/primitives/state-machine";
5
+ import { IllegalTransitionError, assertAndClaim, defineStateMachine } from "@classytic/primitives/state-machine";
6
6
  import { randomUUID } from "node:crypto";
7
7
  import { z } from "zod";
8
+ import { civilDateOf } from "@classytic/primitives/timezone";
9
+ import { CURRENCY_PATTERN } from "@classytic/primitives/currency";
8
10
  import { appendStatus, lastTransitionTo, latestEntry, timeInStatus } from "@classytic/primitives/status-history";
9
11
  //#region src/core/errors.ts
10
12
  /**
@@ -58,6 +60,11 @@ var DuplicateLeadError = class extends CrmError {
58
60
  super("DUPLICATE_LEAD", `A lead with this email / phone already exists`, details);
59
61
  }
60
62
  };
63
+ var UnmanagedSessionError = class extends CrmError {
64
+ constructor(eventType) {
65
+ super("UNMANAGED_SESSION", "ctx.session was provided but no event delivery path exists — inside a host-owned transaction the event would either leak to subscribers before commit or be lost on rollback. Use engine.withTransaction(ctx, fn), wire CrmConfig.outbox, or attach a ctx[PENDING_EVENTS] queue and call flushPending() after commit (PACKAGE_RULES §P8.1).", eventType !== void 0 ? { eventType } : void 0);
66
+ }
67
+ };
61
68
  var CrmModelCollisionError = class extends CrmError {
62
69
  constructor(modelName) {
63
70
  super("CRM_MODEL_COLLISION", `Mongoose model '${modelName}' is already registered on this connection. Another engine (or a hot reload) owns it. Pass { forceRecreate: true } to createCrm() for test/hot-reload fixtures, or use a separate mongoose connection per engine in production (PACKAGE_RULES §21).`, { modelName });
@@ -98,6 +105,21 @@ var InvalidUnsubscribeTokenError = class extends CrmError {
98
105
  super("INVALID_UNSUBSCRIBE_TOKEN", "No subscription matches the supplied unsubscribe token");
99
106
  }
100
107
  };
108
+ var InvalidSequenceTransitionError = class extends CrmError {
109
+ constructor(details) {
110
+ super("INVALID_SEQUENCE_TRANSITION", `Sequence ${details.sequenceId} cannot move from '${details.from}' to '${details.to}'`, details);
111
+ }
112
+ };
113
+ var InvalidEnrollmentTransitionError = class extends CrmError {
114
+ constructor(details) {
115
+ super("INVALID_ENROLLMENT_TRANSITION", `Enrollment ${details.enrollmentId} cannot move from '${details.from}' to '${details.to}'`, details);
116
+ }
117
+ };
118
+ var AlreadyEnrolledError = class extends CrmError {
119
+ constructor(details) {
120
+ super("ALREADY_ENROLLED", `Subject '${details.subjectId}' already has an active enrollment in sequence '${details.sequenceId}'`, details);
121
+ }
122
+ };
101
123
  //#endregion
102
124
  //#region src/models/tenant-field.ts
103
125
  /**
@@ -257,7 +279,7 @@ function canTransitionLead(from, to) {
257
279
  /**
258
280
  * Lead — top-of-funnel inquiry, pre-qualification.
259
281
  */
260
- const statusEntrySchema$3 = new Schema({
282
+ const statusEntrySchema$5 = new Schema({
261
283
  status: {
262
284
  type: String,
263
285
  enum: LEAD_STATUSES,
@@ -331,7 +353,7 @@ function buildLeadSchema(config) {
331
353
  index: true
332
354
  },
333
355
  statusHistory: {
334
- type: [statusEntrySchema$3],
356
+ type: [statusEntrySchema$5],
335
357
  default: []
336
358
  },
337
359
  ownerId: {
@@ -418,7 +440,7 @@ function canTransitionOpportunity(from, to) {
418
440
  * Money amount lives flat (`amount` + `currency`) so `$sum` aggregates work; the repo
419
441
  * surface returns it that way too (no Money translation layer per PACKAGE_RULES §4).
420
442
  */
421
- const statusEntrySchema$2 = new Schema({
443
+ const statusEntrySchema$4 = new Schema({
422
444
  status: {
423
445
  type: String,
424
446
  enum: OPPORTUNITY_STATUSES,
@@ -466,7 +488,7 @@ function buildOpportunitySchema(config) {
466
488
  index: true
467
489
  },
468
490
  statusHistory: {
469
- type: [statusEntrySchema$2],
491
+ type: [statusEntrySchema$4],
470
492
  default: []
471
493
  },
472
494
  amount: { type: Number },
@@ -779,7 +801,7 @@ function canTransitionCampaign(from, to) {
779
801
  * Campaign — a marketing/sales motion targeting a contact list.
780
802
  * Money fields stored flat (amount + currency) for $sum aggregations.
781
803
  */
782
- const statusEntrySchema$1 = new Schema({
804
+ const statusEntrySchema$3 = new Schema({
783
805
  status: {
784
806
  type: String,
785
807
  enum: CAMPAIGN_STATUSES,
@@ -813,7 +835,7 @@ function buildCampaignSchema(config) {
813
835
  index: true
814
836
  },
815
837
  statusHistory: {
816
- type: [statusEntrySchema$1],
838
+ type: [statusEntrySchema$3],
817
839
  default: []
818
840
  },
819
841
  utmSource: {
@@ -928,7 +950,7 @@ function canTransitionSubscription(from, to) {
928
950
  * same connection. TypeScript class + file names retain `Subscription` for
929
951
  * source-level continuity — only the Mongo identity changed in v0.4.0.
930
952
  */
931
- const statusEntrySchema = new Schema({
953
+ const statusEntrySchema$2 = new Schema({
932
954
  status: {
933
955
  type: String,
934
956
  enum: SUBSCRIPTION_STATUSES,
@@ -961,7 +983,7 @@ function buildSubscriptionSchema(config) {
961
983
  index: true
962
984
  },
963
985
  statusHistory: {
964
- type: [statusEntrySchema],
986
+ type: [statusEntrySchema$2],
965
987
  default: []
966
988
  },
967
989
  source: {
@@ -1036,6 +1058,83 @@ function createSubscriptionModel(connection, config) {
1036
1058
  return connection.model("EmailSubscription", buildSubscriptionSchema(config));
1037
1059
  }
1038
1060
  //#endregion
1061
+ //#region src/enums/suppression-reason.ts
1062
+ /** Reasons derived 1:1 from an email engagement event kind. */
1063
+ const EVENT_DERIVED_SUPPRESSION_REASONS = [
1064
+ "bounce",
1065
+ "complaint",
1066
+ "unsubscribe"
1067
+ ];
1068
+ const SUPPRESSION_REASONS = [...EVENT_DERIVED_SUPPRESSION_REASONS, "manual"];
1069
+ //#endregion
1070
+ //#region src/models/suppression.schema.ts
1071
+ /**
1072
+ * Suppression — org-wide "never email this address again" list.
1073
+ *
1074
+ * Subscription unsubscribes are per-campaign; a Suppression row is the
1075
+ * GLOBAL gate a host consults before EVERY outbound email (campaign
1076
+ * fanout, sequence step sends, one-off notifications). One row per
1077
+ * (org, email) — `suppress()` is getOrCreate-idempotent against the
1078
+ * unique index below.
1079
+ *
1080
+ * `subjectKind`/`subjectId` are optional String external refs (rule 7 —
1081
+ * polymorphic across Contact/Lead/host-owned models, so no
1082
+ * ObjectId+refPath). `source` is a free-form origin marker, e.g.
1083
+ * `'campaign:<id>'`, `'sequence:<id>'`, `'api'`, `'import'`.
1084
+ */
1085
+ function buildSuppressionSchema(config) {
1086
+ const schema = new Schema({
1087
+ organizationId: tenantFieldDef(config),
1088
+ email: {
1089
+ type: String,
1090
+ required: true,
1091
+ trim: true,
1092
+ lowercase: true,
1093
+ maxlength: 320
1094
+ },
1095
+ reason: {
1096
+ type: String,
1097
+ enum: SUPPRESSION_REASONS,
1098
+ required: true
1099
+ },
1100
+ source: {
1101
+ type: String,
1102
+ trim: true,
1103
+ maxlength: 200
1104
+ },
1105
+ subjectKind: {
1106
+ type: String,
1107
+ trim: true,
1108
+ maxlength: 50
1109
+ },
1110
+ subjectId: {
1111
+ type: String,
1112
+ trim: true,
1113
+ maxlength: 100
1114
+ },
1115
+ suppressedAt: {
1116
+ type: Date,
1117
+ required: true,
1118
+ default: () => /* @__PURE__ */ new Date()
1119
+ },
1120
+ metadata: { type: Schema.Types.Mixed }
1121
+ }, { timestamps: true });
1122
+ schema.index({
1123
+ organizationId: 1,
1124
+ email: 1
1125
+ }, { unique: true });
1126
+ schema.index({
1127
+ organizationId: 1,
1128
+ reason: 1,
1129
+ suppressedAt: -1
1130
+ });
1131
+ schema.index({
1132
+ organizationId: 1,
1133
+ suppressedAt: -1
1134
+ });
1135
+ return schema;
1136
+ }
1137
+ //#endregion
1039
1138
  //#region src/enums/activity.ts
1040
1139
  /**
1041
1140
  * Activity type vocabulary. The package stores `type` as free-form
@@ -1608,6 +1707,263 @@ function createMessageModel(connection, config) {
1608
1707
  return connection.model("Message", buildMessageSchema(config));
1609
1708
  }
1610
1709
  //#endregion
1710
+ //#region src/enums/sequence-status.ts
1711
+ /**
1712
+ * Email sequence lifecycle.
1713
+ *
1714
+ * draft — being built; no enrollments can be created
1715
+ * active — accepting new enrollments; steps are executing
1716
+ * paused — no new enrollments; running enrollments pause at next step
1717
+ * archived — soft-hidden; historical data preserved
1718
+ */
1719
+ const SEQUENCE_STATUSES = [
1720
+ "draft",
1721
+ "active",
1722
+ "paused",
1723
+ "archived"
1724
+ ];
1725
+ const SEQUENCE_TRANSITIONS = {
1726
+ draft: ["active", "archived"],
1727
+ active: ["paused", "archived"],
1728
+ paused: ["active", "archived"],
1729
+ archived: []
1730
+ };
1731
+ function canTransitionSequence(from, to) {
1732
+ return SEQUENCE_TRANSITIONS[from].includes(to);
1733
+ }
1734
+ //#endregion
1735
+ //#region src/models/sequence.schema.ts
1736
+ /**
1737
+ * Sequence — ordered cadence of SEND_EMAIL and DELAY steps.
1738
+ *
1739
+ * Steps are stored as a JSON array (Mixed) so the host can add new step
1740
+ * types without a schema migration. The discriminator is `step.type`.
1741
+ */
1742
+ const statusEntrySchema$1 = new Schema({
1743
+ status: {
1744
+ type: String,
1745
+ enum: SEQUENCE_STATUSES,
1746
+ required: true
1747
+ },
1748
+ occurredAt: {
1749
+ type: Date,
1750
+ required: true
1751
+ },
1752
+ by: { type: String },
1753
+ note: { type: String }
1754
+ }, { _id: false });
1755
+ function buildSequenceSchema(config) {
1756
+ const schema = new Schema({
1757
+ organizationId: tenantFieldDef(config),
1758
+ name: {
1759
+ type: String,
1760
+ required: true,
1761
+ trim: true,
1762
+ maxlength: 200
1763
+ },
1764
+ description: {
1765
+ type: String,
1766
+ trim: true,
1767
+ maxlength: 1e3
1768
+ },
1769
+ status: {
1770
+ type: String,
1771
+ enum: SEQUENCE_STATUSES,
1772
+ default: "draft",
1773
+ required: true
1774
+ },
1775
+ fromName: {
1776
+ type: String,
1777
+ trim: true,
1778
+ maxlength: 100
1779
+ },
1780
+ fromEmail: {
1781
+ type: String,
1782
+ trim: true,
1783
+ maxlength: 200
1784
+ },
1785
+ steps: {
1786
+ type: [Schema.Types.Mixed],
1787
+ default: []
1788
+ },
1789
+ replyDetectionEnabled: {
1790
+ type: Boolean,
1791
+ default: true
1792
+ },
1793
+ tags: {
1794
+ type: [String],
1795
+ default: []
1796
+ },
1797
+ ownerId: { type: String },
1798
+ metadata: { type: Schema.Types.Mixed },
1799
+ statusHistory: {
1800
+ type: [statusEntrySchema$1],
1801
+ default: []
1802
+ }
1803
+ }, { timestamps: true });
1804
+ schema.index({
1805
+ organizationId: 1,
1806
+ status: 1
1807
+ });
1808
+ schema.index({
1809
+ organizationId: 1,
1810
+ createdAt: -1
1811
+ });
1812
+ return schema;
1813
+ }
1814
+ //#endregion
1815
+ //#region src/enums/enrollment-status.ts
1816
+ /**
1817
+ * Enrollment lifecycle.
1818
+ *
1819
+ * active — steps are executing (or waiting on a delay)
1820
+ * paused — execution halted; can be resumed
1821
+ * completed — all steps ran to completion
1822
+ * exited — removed early (reply, unsubscribe, bounce, manual)
1823
+ */
1824
+ const ENROLLMENT_STATUSES = [
1825
+ "active",
1826
+ "paused",
1827
+ "completed",
1828
+ "exited"
1829
+ ];
1830
+ const ENROLLMENT_TRANSITIONS = {
1831
+ active: [
1832
+ "paused",
1833
+ "completed",
1834
+ "exited"
1835
+ ],
1836
+ paused: ["active", "exited"],
1837
+ completed: [],
1838
+ exited: []
1839
+ };
1840
+ function canTransitionEnrollment(from, to) {
1841
+ return ENROLLMENT_TRANSITIONS[from].includes(to);
1842
+ }
1843
+ //#endregion
1844
+ //#region src/models/enrollment.schema.ts
1845
+ /**
1846
+ * Enrollment — a single subject (lead or contact) progressing through a Sequence.
1847
+ *
1848
+ * `currentStepIndex` tracks which step to execute next. The sequence workflow
1849
+ * reads this field at each iteration so crashes / pauses resume from the
1850
+ * correct position without re-running completed steps.
1851
+ */
1852
+ const ENROLLMENT_SUBJECT_KINDS = ["lead", "contact"];
1853
+ const stepResultSchema = new Schema({
1854
+ stepId: {
1855
+ type: String,
1856
+ required: true
1857
+ },
1858
+ stepType: {
1859
+ type: String,
1860
+ required: true
1861
+ },
1862
+ status: {
1863
+ type: String,
1864
+ enum: [
1865
+ "pending",
1866
+ "sent",
1867
+ "failed",
1868
+ "skipped"
1869
+ ],
1870
+ required: true
1871
+ },
1872
+ sentAt: { type: Date },
1873
+ error: { type: String }
1874
+ }, { _id: false });
1875
+ const statusEntrySchema = new Schema({
1876
+ status: {
1877
+ type: String,
1878
+ enum: ENROLLMENT_STATUSES,
1879
+ required: true
1880
+ },
1881
+ occurredAt: {
1882
+ type: Date,
1883
+ required: true
1884
+ },
1885
+ by: { type: String }
1886
+ }, { _id: false });
1887
+ function buildEnrollmentSchema(config) {
1888
+ const schema = new Schema({
1889
+ organizationId: tenantFieldDef(config),
1890
+ sequenceId: {
1891
+ type: config.tenantFieldType === "objectId" ? Schema.Types.ObjectId : String,
1892
+ required: true,
1893
+ ref: "Sequence"
1894
+ },
1895
+ subjectKind: {
1896
+ type: String,
1897
+ enum: ENROLLMENT_SUBJECT_KINDS,
1898
+ required: true
1899
+ },
1900
+ subjectId: {
1901
+ type: config.tenantFieldType === "objectId" ? Schema.Types.ObjectId : String,
1902
+ required: true
1903
+ },
1904
+ status: {
1905
+ type: String,
1906
+ enum: ENROLLMENT_STATUSES,
1907
+ default: "active",
1908
+ required: true
1909
+ },
1910
+ currentStepIndex: {
1911
+ type: Number,
1912
+ default: 0,
1913
+ min: 0
1914
+ },
1915
+ stepResults: {
1916
+ type: [stepResultSchema],
1917
+ default: []
1918
+ },
1919
+ resumeAt: { type: Date },
1920
+ workflowRunId: { type: String },
1921
+ exitReason: {
1922
+ type: String,
1923
+ enum: [
1924
+ "completed",
1925
+ "reply",
1926
+ "unsubscribe",
1927
+ "bounce",
1928
+ "manual"
1929
+ ]
1930
+ },
1931
+ enrolledAt: {
1932
+ type: Date,
1933
+ required: true,
1934
+ default: () => /* @__PURE__ */ new Date()
1935
+ },
1936
+ completedAt: { type: Date },
1937
+ ownerId: { type: String },
1938
+ metadata: { type: Schema.Types.Mixed },
1939
+ statusHistory: {
1940
+ type: [statusEntrySchema],
1941
+ default: []
1942
+ }
1943
+ }, { timestamps: true });
1944
+ schema.index({
1945
+ organizationId: 1,
1946
+ sequenceId: 1,
1947
+ subjectId: 1,
1948
+ status: 1
1949
+ }, { unique: false });
1950
+ schema.index({
1951
+ organizationId: 1,
1952
+ sequenceId: 1,
1953
+ status: 1
1954
+ });
1955
+ schema.index({
1956
+ organizationId: 1,
1957
+ subjectKind: 1,
1958
+ subjectId: 1
1959
+ });
1960
+ schema.index({
1961
+ organizationId: 1,
1962
+ createdAt: -1
1963
+ });
1964
+ return schema;
1965
+ }
1966
+ //#endregion
1611
1967
  //#region src/models/create-models.ts
1612
1968
  /** Mongoose model names this package registers — drives the §21 collision check. */
1613
1969
  const CRM_MODEL_NAMES = [
@@ -1618,12 +1974,15 @@ const CRM_MODEL_NAMES = [
1618
1974
  "Account",
1619
1975
  "Campaign",
1620
1976
  "EmailSubscription",
1977
+ "Suppression",
1621
1978
  "Activity",
1622
1979
  "Note",
1623
1980
  "LossReason",
1624
1981
  "EmailEvent",
1625
1982
  "Conversation",
1626
- "Message"
1983
+ "Message",
1984
+ "Sequence",
1985
+ "Enrollment"
1627
1986
  ];
1628
1987
  function createCrmModels(connection, config) {
1629
1988
  const forceRecreate = config.forceRecreate ?? false;
@@ -1644,12 +2003,15 @@ function createCrmModels(connection, config) {
1644
2003
  Account: register("Account", buildAccountSchema(config)),
1645
2004
  Campaign: register("Campaign", buildCampaignSchema(config)),
1646
2005
  Subscription: register("EmailSubscription", buildSubscriptionSchema(config)),
2006
+ Suppression: register("Suppression", buildSuppressionSchema(config)),
1647
2007
  Activity: register("Activity", buildActivitySchema(config)),
1648
2008
  Note: register("Note", buildNoteSchema(config)),
1649
2009
  LossReason: register("LossReason", buildLossReasonSchema(config)),
1650
2010
  EmailEvent: register("EmailEvent", buildEmailEventSchema(config)),
1651
2011
  Conversation: register("Conversation", buildConversationSchema(config)),
1652
- Message: register("Message", buildMessageSchema(config))
2012
+ Message: register("Message", buildMessageSchema(config)),
2013
+ Sequence: register("Sequence", buildSequenceSchema(config)),
2014
+ Enrollment: register("Enrollment", buildEnrollmentSchema(config))
1653
2015
  });
1654
2016
  }
1655
2017
  //#endregion
@@ -1688,6 +2050,16 @@ const CRM_EVENTS = {
1688
2050
  SUBSCRIPTION_UNSUBSCRIBED: "crm:subscription.unsubscribed",
1689
2051
  SUBSCRIPTION_BOUNCED: "crm:subscription.bounced",
1690
2052
  SUBSCRIPTION_COMPLAINED: "crm:subscription.complained",
2053
+ SEQUENCE_CREATED: "crm:sequence.created",
2054
+ SEQUENCE_ACTIVATED: "crm:sequence.active",
2055
+ SEQUENCE_PAUSED: "crm:sequence.paused",
2056
+ SEQUENCE_ARCHIVED: "crm:sequence.archived",
2057
+ ENROLLMENT_CREATED: "crm:enrollment.created",
2058
+ ENROLLMENT_STEP_SENT: "crm:enrollment.step_sent",
2059
+ ENROLLMENT_PAUSED: "crm:enrollment.paused",
2060
+ ENROLLMENT_RESUMED: "crm:enrollment.resumed",
2061
+ ENROLLMENT_COMPLETED: "crm:enrollment.completed",
2062
+ ENROLLMENT_EXITED: "crm:enrollment.exited",
1691
2063
  EMAIL_EVENT_OPENED: "crm:email_event.opened",
1692
2064
  EMAIL_EVENT_CLICKED: "crm:email_event.clicked",
1693
2065
  EMAIL_EVENT_BOUNCED: "crm:email_event.bounced",
@@ -1706,6 +2078,8 @@ const CRM_EVENTS = {
1706
2078
  OPPORTUNITY_EXTENDED: "crm:opportunity.extended",
1707
2079
  ACTIVITY_RESCHEDULED: "crm:activity.rescheduled",
1708
2080
  ACTIVITY_OUTCOME_RECORDED: "crm:activity.outcome_recorded",
2081
+ SUPPRESSION_ADDED: "crm:suppression.added",
2082
+ SUPPRESSION_REMOVED: "crm:suppression.removed",
1709
2083
  CONVERSATION_CREATED: "crm:conversation.created",
1710
2084
  CONVERSATION_ASSIGNED: "crm:conversation.assigned",
1711
2085
  CONVERSATION_RESOLVED: "crm:conversation.resolved",
@@ -1778,7 +2152,7 @@ var PipelineRepository = class extends Repository {
1778
2152
  async archive(id, ctx) {
1779
2153
  const updated = await this.update(id, { isArchived: true }, ctx);
1780
2154
  if (!updated) throw new NotFoundError("Pipeline", id);
1781
- await this.events.publish(createEvent(CRM_EVENTS.PIPELINE_ARCHIVED, { pipelineId: id }, ctx));
2155
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.PIPELINE_ARCHIVED, { pipelineId: id }, ctx));
1782
2156
  return updated;
1783
2157
  }
1784
2158
  };
@@ -1844,6 +2218,24 @@ const ACTIVITY_MACHINE = defineStateMachine({
1844
2218
  activityId: entityId
1845
2219
  })
1846
2220
  });
2221
+ const SEQUENCE_MACHINE = defineStateMachine({
2222
+ name: "Sequence",
2223
+ transitions: SEQUENCE_TRANSITIONS,
2224
+ errorFactory: ({ entityId, from, to }) => new InvalidSequenceTransitionError({
2225
+ from,
2226
+ to,
2227
+ sequenceId: entityId
2228
+ })
2229
+ });
2230
+ const ENROLLMENT_MACHINE = defineStateMachine({
2231
+ name: "Enrollment",
2232
+ transitions: ENROLLMENT_TRANSITIONS,
2233
+ errorFactory: ({ entityId, from, to }) => new InvalidEnrollmentTransitionError({
2234
+ from,
2235
+ to,
2236
+ enrollmentId: entityId
2237
+ })
2238
+ });
1847
2239
  //#endregion
1848
2240
  //#region src/shared/calculators/scoring.ts
1849
2241
  /**
@@ -1913,12 +2305,12 @@ var LeadRepository = class extends Repository {
1913
2305
  let result = created;
1914
2306
  if (score !== 0) {
1915
2307
  result = await this.update(String(created._id), { score }, ctx) ?? created;
1916
- await this.events.publish(createEvent(CRM_EVENTS.LEAD_SCORED, {
2308
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.LEAD_SCORED, {
1917
2309
  leadId: String(created._id),
1918
2310
  score
1919
2311
  }, ctx));
1920
2312
  }
1921
- await this.events.publish(createEvent(CRM_EVENTS.LEAD_CREATED, {
2313
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.LEAD_CREATED, {
1922
2314
  leadId: String(result._id),
1923
2315
  email: result.email,
1924
2316
  score: result.score
@@ -1937,7 +2329,7 @@ var LeadRepository = class extends Repository {
1937
2329
  note: reason,
1938
2330
  patch: { disqualifyReason: reason }
1939
2331
  }, ctx);
1940
- await this.events.publish(createEvent(CRM_EVENTS.LEAD_DISQUALIFIED, {
2332
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.LEAD_DISQUALIFIED, {
1941
2333
  leadId: id,
1942
2334
  reason
1943
2335
  }, ctx));
@@ -1959,7 +2351,7 @@ var LeadRepository = class extends Repository {
1959
2351
  if (!await this.getById(id, ctx)) throw new NotFoundError("Lead", id);
1960
2352
  if (!await this.update(id, patch, ctx)) throw new NotFoundError("Lead", id);
1961
2353
  const fields = Object.keys(patch);
1962
- await this.events.publish(createEvent(CRM_EVENTS.LEAD_ENRICHED, {
2354
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.LEAD_ENRICHED, {
1963
2355
  leadId: id,
1964
2356
  fields,
1965
2357
  ...extras.provider !== void 0 ? { provider: extras.provider } : {}
@@ -1983,7 +2375,7 @@ var LeadRepository = class extends Repository {
1983
2375
  if (!current) throw new NotFoundError("Lead", id);
1984
2376
  return current;
1985
2377
  }
1986
- await this.events.publish(createEvent(CRM_EVENTS.LEAD_ASSIGNED, {
2378
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.LEAD_ASSIGNED, {
1987
2379
  leadId: id,
1988
2380
  ownerId,
1989
2381
  ...before.ownerId !== void 0 ? { previousOwnerId: before.ownerId } : {}
@@ -2001,7 +2393,7 @@ var LeadRepository = class extends Repository {
2001
2393
  if (score === lead.score) return lead;
2002
2394
  const updated = await this.update(id, { score }, ctx);
2003
2395
  if (!updated) throw new NotFoundError("Lead", id);
2004
- await this.events.publish(createEvent(CRM_EVENTS.LEAD_SCORED, {
2396
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.LEAD_SCORED, {
2005
2397
  leadId: id,
2006
2398
  score
2007
2399
  }, ctx));
@@ -2031,7 +2423,7 @@ var LeadRepository = class extends Repository {
2031
2423
  to: target,
2032
2424
  leadId: id
2033
2425
  });
2034
- await this.events.publish(createEvent(CRM_EVENTS.LEAD_STATUS_CHANGED, {
2426
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.LEAD_STATUS_CHANGED, {
2035
2427
  leadId: id,
2036
2428
  from: existing.status,
2037
2429
  to: target
@@ -2097,7 +2489,7 @@ var OpportunityRepository = class extends Repository {
2097
2489
  ...input.metadata !== void 0 ? { metadata: input.metadata } : {}
2098
2490
  };
2099
2491
  const created = await this.create(draft, ctx);
2100
- await this.events.publish(createEvent(CRM_EVENTS.OPPORTUNITY_CREATED, {
2492
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.OPPORTUNITY_CREATED, {
2101
2493
  opportunityId: String(created._id),
2102
2494
  pipelineId: input.pipelineId,
2103
2495
  stageId: String(stage._id)
@@ -2141,7 +2533,7 @@ var OpportunityRepository = class extends Repository {
2141
2533
  stageId: String(target._id)
2142
2534
  });
2143
2535
  const event = target.sequence > current.sequence ? CRM_EVENTS.OPPORTUNITY_STAGE_ADVANCED : CRM_EVENTS.OPPORTUNITY_STAGE_REGRESSED;
2144
- await this.events.publish(createEvent(event, {
2536
+ await this.events.dispatch(ctx, createEvent(event, {
2145
2537
  opportunityId: id,
2146
2538
  from: {
2147
2539
  stageId: String(current._id),
@@ -2163,7 +2555,7 @@ var OpportunityRepository = class extends Repository {
2163
2555
  ...input.by !== void 0 ? { by: input.by } : {},
2164
2556
  ...input.note !== void 0 ? { note: input.note } : {}
2165
2557
  }, ctx);
2166
- await this.events.publish(createEvent(CRM_EVENTS.OPPORTUNITY_WON, {
2558
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.OPPORTUNITY_WON, {
2167
2559
  opportunityId: id,
2168
2560
  amount: updated.amount
2169
2561
  }, ctx));
@@ -2178,7 +2570,7 @@ var OpportunityRepository = class extends Repository {
2178
2570
  ...input.by !== void 0 ? { by: input.by } : {},
2179
2571
  ...input.note !== void 0 ? { note: input.note } : {}
2180
2572
  }, ctx);
2181
- await this.events.publish(createEvent(CRM_EVENTS.OPPORTUNITY_LOST, {
2573
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.OPPORTUNITY_LOST, {
2182
2574
  opportunityId: id,
2183
2575
  lostReasonId: updated.lostReasonId
2184
2576
  }, ctx));
@@ -2201,7 +2593,7 @@ var OpportunityRepository = class extends Repository {
2201
2593
  if (!current) throw new NotFoundError("Opportunity", id);
2202
2594
  return current;
2203
2595
  }
2204
- await this.events.publish(createEvent(CRM_EVENTS.OPPORTUNITY_ASSIGNED, {
2596
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.OPPORTUNITY_ASSIGNED, {
2205
2597
  opportunityId: id,
2206
2598
  ownerId,
2207
2599
  ...before.ownerId !== void 0 ? { previousOwnerId: before.ownerId } : {}
@@ -2225,7 +2617,7 @@ var OpportunityRepository = class extends Repository {
2225
2617
  if (amount === existing.amount) return existing;
2226
2618
  const updated = await this.update(id, { amount }, ctx);
2227
2619
  if (!updated) throw new NotFoundError("Opportunity", id);
2228
- await this.events.publish(createEvent(CRM_EVENTS.OPPORTUNITY_AMOUNT_CHANGED, {
2620
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.OPPORTUNITY_AMOUNT_CHANGED, {
2229
2621
  opportunityId: id,
2230
2622
  amount,
2231
2623
  ...existing.amount !== void 0 ? { previousAmount: existing.amount } : {},
@@ -2252,7 +2644,7 @@ var OpportunityRepository = class extends Repository {
2252
2644
  if (previous && newDate <= previous) return existing;
2253
2645
  const updated = await this.update(id, { expectedCloseAt: newDate }, ctx);
2254
2646
  if (!updated) throw new NotFoundError("Opportunity", id);
2255
- await this.events.publish(createEvent(CRM_EVENTS.OPPORTUNITY_EXTENDED, {
2647
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.OPPORTUNITY_EXTENDED, {
2256
2648
  opportunityId: id,
2257
2649
  expectedCloseAt: newDate.toISOString(),
2258
2650
  ...previous ? { previousExpectedCloseAt: previous.toISOString() } : {},
@@ -2269,7 +2661,7 @@ var OpportunityRepository = class extends Repository {
2269
2661
  ...by !== void 0 ? { by } : {},
2270
2662
  ...note !== void 0 ? { note } : {}
2271
2663
  }, ctx);
2272
- await this.events.publish(createEvent(CRM_EVENTS.OPPORTUNITY_ABANDONED, { opportunityId: id }, ctx));
2664
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.OPPORTUNITY_ABANDONED, { opportunityId: id }, ctx));
2273
2665
  return updated;
2274
2666
  }
2275
2667
  async _closeStatus(id, target, patch, extras, ctx) {
@@ -2366,7 +2758,7 @@ var ContactRepository = class extends Repository {
2366
2758
  */
2367
2759
  async createContact(input, ctx) {
2368
2760
  const created = await this.create(input, ctx);
2369
- await this.events.publish(createEvent(CRM_EVENTS.CONTACT_CREATED, {
2761
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.CONTACT_CREATED, {
2370
2762
  contactId: String(created._id),
2371
2763
  email: created.contact?.email
2372
2764
  }, ctx));
@@ -2381,7 +2773,7 @@ var ContactRepository = class extends Repository {
2381
2773
  if (!await this.getById(id, ctx)) throw new NotFoundError("Contact", id);
2382
2774
  const updated = await this.update(id, patch, ctx);
2383
2775
  if (!updated) throw new NotFoundError("Contact", id);
2384
- await this.events.publish(createEvent(CRM_EVENTS.CONTACT_ENRICHED, {
2776
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.CONTACT_ENRICHED, {
2385
2777
  contactId: id,
2386
2778
  fields: Object.keys(patch),
2387
2779
  ...extras.provider !== void 0 ? { provider: extras.provider } : {}
@@ -2402,7 +2794,7 @@ var ContactRepository = class extends Repository {
2402
2794
  if (!current) throw new NotFoundError("Contact", id);
2403
2795
  return current;
2404
2796
  }
2405
- await this.events.publish(createEvent(CRM_EVENTS.CONTACT_ASSIGNED, {
2797
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.CONTACT_ASSIGNED, {
2406
2798
  contactId: id,
2407
2799
  ownerId,
2408
2800
  ...before.ownerId !== void 0 ? { previousOwnerId: before.ownerId } : {}
@@ -2452,7 +2844,7 @@ var ContactRepository = class extends Repository {
2452
2844
  const updated = await this.update(primaryId, patch, ctx);
2453
2845
  if (!updated) throw new NotFoundError("Contact", primaryId);
2454
2846
  await this.delete(secondaryId, ctx);
2455
- await this.events.publish(createEvent(CRM_EVENTS.CONTACT_MERGED, {
2847
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.CONTACT_MERGED, {
2456
2848
  primaryId,
2457
2849
  secondaryId,
2458
2850
  strategy
@@ -2484,7 +2876,7 @@ var AccountRepository = class extends Repository {
2484
2876
  }
2485
2877
  async createAccount(input, ctx) {
2486
2878
  const created = await this.create(input, ctx);
2487
- await this.events.publish(createEvent(CRM_EVENTS.ACCOUNT_CREATED, {
2879
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ACCOUNT_CREATED, {
2488
2880
  accountId: String(created._id),
2489
2881
  name: created.name
2490
2882
  }, ctx));
@@ -2494,7 +2886,7 @@ var AccountRepository = class extends Repository {
2494
2886
  if (!await this.getById(id, ctx)) throw new NotFoundError("Account", id);
2495
2887
  const updated = await this.update(id, patch, ctx);
2496
2888
  if (!updated) throw new NotFoundError("Account", id);
2497
- await this.events.publish(createEvent(CRM_EVENTS.ACCOUNT_ENRICHED, {
2889
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ACCOUNT_ENRICHED, {
2498
2890
  accountId: id,
2499
2891
  fields: Object.keys(patch),
2500
2892
  ...extras.provider !== void 0 ? { provider: extras.provider } : {}
@@ -2514,7 +2906,7 @@ var AccountRepository = class extends Repository {
2514
2906
  if (!current) throw new NotFoundError("Account", id);
2515
2907
  return current;
2516
2908
  }
2517
- await this.events.publish(createEvent(CRM_EVENTS.ACCOUNT_ASSIGNED, {
2909
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ACCOUNT_ASSIGNED, {
2518
2910
  accountId: id,
2519
2911
  ownerId,
2520
2912
  ...before.ownerId !== void 0 ? { previousOwnerId: before.ownerId } : {}
@@ -2543,7 +2935,7 @@ var AccountRepository = class extends Repository {
2543
2935
  const updated = await this.update(primaryId, patch, ctx);
2544
2936
  if (!updated) throw new NotFoundError("Account", primaryId);
2545
2937
  await this.delete(secondaryId, ctx);
2546
- await this.events.publish(createEvent(CRM_EVENTS.ACCOUNT_MERGED, {
2938
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ACCOUNT_MERGED, {
2547
2939
  primaryId,
2548
2940
  secondaryId,
2549
2941
  strategy
@@ -2587,7 +2979,7 @@ var CampaignRepository = class extends Repository {
2587
2979
  if (this.rejectDuplicates && input.name && isDuplicateKeyError(err)) throw new DuplicateCampaignError(input.name);
2588
2980
  throw err;
2589
2981
  }
2590
- await this.events.publish(createEvent(CRM_EVENTS.CAMPAIGN_CREATED, {
2982
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.CAMPAIGN_CREATED, {
2591
2983
  campaignId: String(created._id),
2592
2984
  name: created.name
2593
2985
  }, ctx));
@@ -2595,7 +2987,7 @@ var CampaignRepository = class extends Repository {
2595
2987
  }
2596
2988
  async activate(id, by, ctx) {
2597
2989
  const updated = await this._transition(id, "active", { ...by !== void 0 ? { by } : {} }, ctx);
2598
- await this.events.publish(createEvent(CRM_EVENTS.CAMPAIGN_ACTIVATED, { campaignId: id }, ctx));
2990
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.CAMPAIGN_ACTIVATED, { campaignId: id }, ctx));
2599
2991
  return updated;
2600
2992
  }
2601
2993
  async pause(id, reason, by, ctx) {
@@ -2603,7 +2995,7 @@ var CampaignRepository = class extends Repository {
2603
2995
  ...by !== void 0 ? { by } : {},
2604
2996
  ...reason !== void 0 ? { note: reason } : {}
2605
2997
  }, ctx);
2606
- await this.events.publish(createEvent(CRM_EVENTS.CAMPAIGN_PAUSED, {
2998
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.CAMPAIGN_PAUSED, {
2607
2999
  campaignId: id,
2608
3000
  reason
2609
3001
  }, ctx));
@@ -2611,13 +3003,13 @@ var CampaignRepository = class extends Repository {
2611
3003
  }
2612
3004
  async resume(id, by, ctx) {
2613
3005
  const updated = await this._transition(id, "active", { ...by !== void 0 ? { by } : {} }, ctx);
2614
- await this.events.publish(createEvent(CRM_EVENTS.CAMPAIGN_RESUMED, { campaignId: id }, ctx));
3006
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.CAMPAIGN_RESUMED, { campaignId: id }, ctx));
2615
3007
  return updated;
2616
3008
  }
2617
3009
  async complete(id, extras = {}, ctx) {
2618
3010
  const patch = extras.actualRevenueAmount !== void 0 ? { actualRevenueAmount: extras.actualRevenueAmount } : {};
2619
3011
  const updated = await this._transition(id, "completed", extras, ctx, patch);
2620
- await this.events.publish(createEvent(CRM_EVENTS.CAMPAIGN_COMPLETED, {
3012
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.CAMPAIGN_COMPLETED, {
2621
3013
  campaignId: id,
2622
3014
  actualRevenueAmount: updated.actualRevenueAmount
2623
3015
  }, ctx));
@@ -2625,7 +3017,7 @@ var CampaignRepository = class extends Repository {
2625
3017
  }
2626
3018
  async archive(id, by, ctx) {
2627
3019
  const updated = await this._transition(id, "archived", { ...by !== void 0 ? { by } : {} }, ctx);
2628
- await this.events.publish(createEvent(CRM_EVENTS.CAMPAIGN_ARCHIVED, { campaignId: id }, ctx));
3020
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.CAMPAIGN_ARCHIVED, { campaignId: id }, ctx));
2629
3021
  return updated;
2630
3022
  }
2631
3023
  async _transition(id, target, extras, ctx, patch = {}) {
@@ -2716,7 +3108,7 @@ var SubscriptionRepository = class extends Repository {
2716
3108
  });
2717
3109
  throw err;
2718
3110
  }
2719
- await this.events.publish(createEvent(CRM_EVENTS.SUBSCRIPTION_CREATED, {
3111
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.SUBSCRIPTION_CREATED, {
2720
3112
  subscriptionId: String(sub._id),
2721
3113
  contactId: sub.contactId,
2722
3114
  campaignId: sub.campaignId,
@@ -2729,7 +3121,7 @@ var SubscriptionRepository = class extends Repository {
2729
3121
  if (!sub || sub.status !== "pending") throw new InvalidConfirmationTokenError();
2730
3122
  const confirmedAt = /* @__PURE__ */ new Date();
2731
3123
  const updated = await this._transition(String(sub._id), "confirmed", { patch: { confirmedAt } }, ctx);
2732
- await this.events.publish(createEvent(CRM_EVENTS.SUBSCRIPTION_CONFIRMED, {
3124
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.SUBSCRIPTION_CONFIRMED, {
2733
3125
  subscriptionId: String(sub._id),
2734
3126
  contactId: sub.contactId,
2735
3127
  campaignId: sub.campaignId,
@@ -2749,7 +3141,7 @@ var SubscriptionRepository = class extends Repository {
2749
3141
  ...reason !== void 0 ? { unsubscribeReason: reason } : {}
2750
3142
  }
2751
3143
  }, ctx);
2752
- await this.events.publish(createEvent(CRM_EVENTS.SUBSCRIPTION_UNSUBSCRIBED, {
3144
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.SUBSCRIPTION_UNSUBSCRIBED, {
2753
3145
  subscriptionId: String(sub._id),
2754
3146
  contactId: sub.contactId,
2755
3147
  campaignId: sub.campaignId,
@@ -2769,7 +3161,7 @@ var SubscriptionRepository = class extends Repository {
2769
3161
  ...reason !== void 0 ? { bounceReason: reason } : {}
2770
3162
  }
2771
3163
  }, ctx);
2772
- await this.events.publish(createEvent(CRM_EVENTS.SUBSCRIPTION_BOUNCED, {
3164
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.SUBSCRIPTION_BOUNCED, {
2773
3165
  subscriptionId: id,
2774
3166
  contactId: existing.contactId,
2775
3167
  campaignId: existing.campaignId,
@@ -2783,7 +3175,7 @@ var SubscriptionRepository = class extends Repository {
2783
3175
  if (existing.status === "complained") return existing;
2784
3176
  const complainedAt = /* @__PURE__ */ new Date();
2785
3177
  const updated = await this._transition(id, "complained", { patch: { complainedAt } }, ctx);
2786
- await this.events.publish(createEvent(CRM_EVENTS.SUBSCRIPTION_COMPLAINED, {
3178
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.SUBSCRIPTION_COMPLAINED, {
2787
3179
  subscriptionId: id,
2788
3180
  contactId: existing.contactId,
2789
3181
  campaignId: existing.campaignId
@@ -2818,6 +3210,78 @@ var SubscriptionRepository = class extends Repository {
2818
3210
  }
2819
3211
  };
2820
3212
  //#endregion
3213
+ //#region src/repositories/suppression.repository.ts
3214
+ function normalizeEmail(email) {
3215
+ return email.trim().toLowerCase();
3216
+ }
3217
+ var SuppressionRepository = class extends Repository {
3218
+ events;
3219
+ constructor(model, plugins, deps, pagination) {
3220
+ super(model, plugins, pagination);
3221
+ this.events = deps.events;
3222
+ }
3223
+ /**
3224
+ * Idempotently add `email` to the org's suppression list. Atomic
3225
+ * `getOrCreate` on the unique (org, email) index — of N concurrent
3226
+ * calls exactly one inserts. Emits `crm:suppression.added` only for
3227
+ * the inserting call; an existing row is returned unchanged, no event.
3228
+ */
3229
+ async suppress(email, input, ctx) {
3230
+ const normalized = normalizeEmail(email);
3231
+ const draft = {
3232
+ email: normalized,
3233
+ reason: input.reason,
3234
+ suppressedAt: /* @__PURE__ */ new Date(),
3235
+ ...input.source !== void 0 ? { source: input.source } : {},
3236
+ ...input.subjectKind !== void 0 ? { subjectKind: input.subjectKind } : {},
3237
+ ...input.subjectId !== void 0 ? { subjectId: input.subjectId } : {},
3238
+ ...input.metadata !== void 0 ? { metadata: input.metadata } : {}
3239
+ };
3240
+ const { doc, created } = await this.getOrCreate({ email: normalized }, draft, ctx);
3241
+ if (created) await this.events.dispatch(ctx, createEvent(CRM_EVENTS.SUPPRESSION_ADDED, {
3242
+ email: normalized,
3243
+ reason: input.reason,
3244
+ ...input.source !== void 0 ? { source: input.source } : {}
3245
+ }, ctx));
3246
+ return doc;
3247
+ }
3248
+ /** True when `email` is on the org's suppression list. */
3249
+ async isSuppressed(email, ctx) {
3250
+ return await this.exists({ email: normalizeEmail(email) }, ctx) !== null;
3251
+ }
3252
+ /**
3253
+ * Return the suppressed subset of `emails` (normalized) as a Set —
3254
+ * ONE `$in` query, built for the campaign-fanout hot path:
3255
+ *
3256
+ * ```ts
3257
+ * const blocked = await suppression.filterSuppressed(recipients, ctx);
3258
+ * const sendable = recipients.filter((e) => !blocked.has(e.toLowerCase()));
3259
+ * ```
3260
+ */
3261
+ async filterSuppressed(emails, ctx) {
3262
+ if (emails.length === 0) return /* @__PURE__ */ new Set();
3263
+ const normalized = [...new Set(emails.map(normalizeEmail))];
3264
+ const rows = await this.findAll({ email: { $in: normalized } }, {
3265
+ ...ctx ?? {},
3266
+ select: "email",
3267
+ lean: true
3268
+ });
3269
+ return new Set(rows.map((row) => row.email));
3270
+ }
3271
+ /**
3272
+ * Remove `email` from the org's suppression list. Single atomic
3273
+ * `deleteMany` on the (unique) email key — emits
3274
+ * `crm:suppression.removed` only when a row was actually removed.
3275
+ * Returns whether a row was removed.
3276
+ */
3277
+ async unsuppress(email, ctx) {
3278
+ const normalized = normalizeEmail(email);
3279
+ const removed = (await this.deleteMany({ email: normalized }, ctx ?? {})).deletedCount > 0;
3280
+ if (removed) await this.events.dispatch(ctx, createEvent(CRM_EVENTS.SUPPRESSION_REMOVED, { email: normalized }, ctx));
3281
+ return removed;
3282
+ }
3283
+ };
3284
+ //#endregion
2821
3285
  //#region src/repositories/activity.repository.ts
2822
3286
  var ActivityRepository = class extends Repository {
2823
3287
  events;
@@ -2831,7 +3295,7 @@ var ActivityRepository = class extends Repository {
2831
3295
  status: "planned"
2832
3296
  };
2833
3297
  const created = await this.create(draft, ctx);
2834
- await this.events.publish(createEvent(CRM_EVENTS.ACTIVITY_PLANNED, {
3298
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ACTIVITY_PLANNED, {
2835
3299
  activityId: String(created._id),
2836
3300
  type: created.type,
2837
3301
  subjectKind: created.subjectKind,
@@ -2845,7 +3309,7 @@ var ActivityRepository = class extends Repository {
2845
3309
  async complete(id, completedAt, ctx) {
2846
3310
  const ts = completedAt ?? /* @__PURE__ */ new Date();
2847
3311
  const updated = await this._transition(id, "completed", { completedAt: ts }, ctx);
2848
- await this.events.publish(createEvent(CRM_EVENTS.ACTIVITY_COMPLETED, {
3312
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ACTIVITY_COMPLETED, {
2849
3313
  activityId: id,
2850
3314
  completedAt: ts.toISOString()
2851
3315
  }, ctx));
@@ -2853,7 +3317,7 @@ var ActivityRepository = class extends Repository {
2853
3317
  }
2854
3318
  async cancel(id, reason, ctx) {
2855
3319
  const updated = await this._transition(id, "cancelled", reason ? { metadata: { cancelReason: reason } } : {}, ctx);
2856
- await this.events.publish(createEvent(CRM_EVENTS.ACTIVITY_CANCELLED, {
3320
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ACTIVITY_CANCELLED, {
2857
3321
  activityId: id,
2858
3322
  ...reason !== void 0 ? { reason } : {}
2859
3323
  }, ctx));
@@ -2875,7 +3339,7 @@ var ActivityRepository = class extends Repository {
2875
3339
  const previous = existing.dueDate;
2876
3340
  const updated = await this.update(id, { dueDate: newDueDate }, ctx);
2877
3341
  if (!updated) throw new NotFoundError("Activity", id);
2878
- await this.events.publish(createEvent(CRM_EVENTS.ACTIVITY_RESCHEDULED, {
3342
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ACTIVITY_RESCHEDULED, {
2879
3343
  activityId: id,
2880
3344
  dueDate: newDueDate.toISOString(),
2881
3345
  ...previous ? { previousDueDate: previous.toISOString() } : {},
@@ -2902,7 +3366,7 @@ var ActivityRepository = class extends Repository {
2902
3366
  };
2903
3367
  const updated = await this.update(id, { metadata: mergedMetadata }, ctx);
2904
3368
  if (!updated) throw new NotFoundError("Activity", id);
2905
- await this.events.publish(createEvent(CRM_EVENTS.ACTIVITY_OUTCOME_RECORDED, {
3369
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ACTIVITY_OUTCOME_RECORDED, {
2906
3370
  activityId: id,
2907
3371
  outcome
2908
3372
  }, ctx));
@@ -2959,7 +3423,7 @@ var NoteRepository = class extends Repository {
2959
3423
  }
2960
3424
  async addNote(input, ctx) {
2961
3425
  const created = await this.create(input, ctx);
2962
- await this.events.publish(createEvent(CRM_EVENTS.NOTE_ADDED, {
3426
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.NOTE_ADDED, {
2963
3427
  noteId: String(created._id),
2964
3428
  subjectKind: created.subjectKind,
2965
3429
  subjectId: String(created.subjectId)
@@ -3025,7 +3489,7 @@ var EmailEventRepository = class extends Repository {
3025
3489
  ...input.metadata !== void 0 ? { metadata: input.metadata } : {}
3026
3490
  };
3027
3491
  const created = await this.create(draft, ctx);
3028
- await this.events.publish(createEvent(KIND_TO_EVENT[kind], {
3492
+ await this.events.dispatch(ctx, createEvent(KIND_TO_EVENT[kind], {
3029
3493
  eventId: String(created._id),
3030
3494
  kind,
3031
3495
  subjectKind: input.subjectKind,
@@ -3055,7 +3519,7 @@ var ConversationRepository = class extends Repository {
3055
3519
  ...input.metadata !== void 0 ? { metadata: input.metadata } : {}
3056
3520
  };
3057
3521
  const created = await this.create(draft, ctx);
3058
- await this.events.publish(createEvent(CRM_EVENTS.CONVERSATION_CREATED, {
3522
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.CONVERSATION_CREATED, {
3059
3523
  conversationId: String(created._id),
3060
3524
  channel: created.channel,
3061
3525
  participantKind: created.participantKind,
@@ -3076,7 +3540,7 @@ var ConversationRepository = class extends Repository {
3076
3540
  if (!current) throw new NotFoundError("Conversation", id);
3077
3541
  return current;
3078
3542
  }
3079
- await this.events.publish(createEvent(CRM_EVENTS.CONVERSATION_ASSIGNED, {
3543
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.CONVERSATION_ASSIGNED, {
3080
3544
  conversationId: id,
3081
3545
  assigneeId,
3082
3546
  ...before.assigneeId !== void 0 ? { previousAssigneeId: before.assigneeId } : {}
@@ -3114,7 +3578,7 @@ var ConversationRepository = class extends Repository {
3114
3578
  if (existing.status === next) return existing;
3115
3579
  const updated = await this.update(id, { status: next }, ctx);
3116
3580
  if (!updated) throw new NotFoundError("Conversation", id);
3117
- await this.events.publish(createEvent(eventType, {
3581
+ await this.events.dispatch(ctx, createEvent(eventType, {
3118
3582
  conversationId: id,
3119
3583
  status: next,
3120
3584
  previousStatus: existing.status
@@ -3166,7 +3630,7 @@ var MessageRepository = class extends Repository {
3166
3630
  direction,
3167
3631
  sentAt
3168
3632
  }, ctx);
3169
- await this.events.publish(createEvent(eventType, {
3633
+ await this.events.dispatch(ctx, createEvent(eventType, {
3170
3634
  messageId: String(created._id),
3171
3635
  conversationId: input.conversationId,
3172
3636
  direction,
@@ -3191,6 +3655,230 @@ var MessageRepository = class extends Repository {
3191
3655
  }
3192
3656
  };
3193
3657
  //#endregion
3658
+ //#region src/repositories/sequence.repository.ts
3659
+ var SequenceRepository = class extends Repository {
3660
+ events;
3661
+ constructor(model, plugins, deps, pagination) {
3662
+ super(model, plugins, pagination);
3663
+ this.events = deps.events;
3664
+ }
3665
+ async activate(id, ctx) {
3666
+ const doc = await this._transition(id, "active", ctx);
3667
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.SEQUENCE_ACTIVATED, { sequenceId: id }, ctx));
3668
+ return doc;
3669
+ }
3670
+ async activateFromPaused(id, ctx) {
3671
+ return this.activate(id, ctx);
3672
+ }
3673
+ async pause(id, ctx) {
3674
+ const doc = await this._transition(id, "paused", ctx);
3675
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.SEQUENCE_PAUSED, { sequenceId: id }, ctx));
3676
+ return doc;
3677
+ }
3678
+ async archive(id, ctx) {
3679
+ const doc = await this._transition(id, "archived", ctx);
3680
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.SEQUENCE_ARCHIVED, { sequenceId: id }, ctx));
3681
+ return doc;
3682
+ }
3683
+ async _transition(id, target, ctx) {
3684
+ const existing = await this.getById(id, ctx);
3685
+ if (!existing) throw new NotFoundError("Sequence", id);
3686
+ SEQUENCE_MACHINE.assertTransition(id, existing.status, target);
3687
+ const entry = {
3688
+ status: target,
3689
+ occurredAt: /* @__PURE__ */ new Date(),
3690
+ ...ctx?.actorId !== void 0 ? { by: String(ctx.actorId) } : {}
3691
+ };
3692
+ const updated = await assertAndClaim(SEQUENCE_MACHINE, this, id, {
3693
+ from: existing.status,
3694
+ to: target,
3695
+ patch: { statusHistory: [...existing.statusHistory ?? [], entry] },
3696
+ options: ctx
3697
+ });
3698
+ if (!updated) throw new InvalidSequenceTransitionError({
3699
+ from: (await this.getById(id, ctx))?.status ?? existing.status,
3700
+ to: target,
3701
+ sequenceId: id
3702
+ });
3703
+ return updated;
3704
+ }
3705
+ };
3706
+ //#endregion
3707
+ //#region src/repositories/enrollment.repository.ts
3708
+ var EnrollmentRepository = class extends Repository {
3709
+ events;
3710
+ sequences;
3711
+ constructor(model, plugins, deps, pagination) {
3712
+ super(model, plugins, pagination);
3713
+ this.events = deps.events;
3714
+ this.sequences = deps.sequences;
3715
+ }
3716
+ /** Create a new enrollment — throws AlreadyEnrolledError if an active one exists. */
3717
+ async enroll(input, ctx) {
3718
+ const { sequenceId, subjectKind, subjectId, workflowRunId, ownerId, metadata } = input;
3719
+ if (await this.getOne({
3720
+ sequenceId,
3721
+ subjectId,
3722
+ status: { $in: ["active", "paused"] }
3723
+ }, ctx)) throw new AlreadyEnrolledError({
3724
+ sequenceId,
3725
+ subjectId
3726
+ });
3727
+ const doc = await this.create({
3728
+ organizationId: ctx?.organizationId,
3729
+ sequenceId,
3730
+ subjectKind,
3731
+ subjectId,
3732
+ status: "active",
3733
+ currentStepIndex: 0,
3734
+ stepResults: [],
3735
+ enrolledAt: /* @__PURE__ */ new Date(),
3736
+ ...workflowRunId ? { workflowRunId } : {},
3737
+ ...ownerId ? { ownerId } : {},
3738
+ ...metadata ? { metadata } : {},
3739
+ statusHistory: [{
3740
+ status: "active",
3741
+ occurredAt: /* @__PURE__ */ new Date()
3742
+ }]
3743
+ }, ctx);
3744
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ENROLLMENT_CREATED, {
3745
+ enrollmentId: String(doc._id),
3746
+ sequenceId,
3747
+ subjectId,
3748
+ subjectKind
3749
+ }, ctx));
3750
+ return doc;
3751
+ }
3752
+ /** Set workflowRunId after the workflow has started. */
3753
+ async setWorkflowRunId(id, workflowRunId, ctx) {
3754
+ await this.update(id, { workflowRunId }, ctx);
3755
+ }
3756
+ /** Atomically advance to the next step and record the result (CAS). */
3757
+ async advanceStep(id, nextStepIndex, stepResult, ctx) {
3758
+ return this.claim(id, {
3759
+ field: "currentStepIndex",
3760
+ from: nextStepIndex - 1,
3761
+ to: nextStepIndex,
3762
+ where: { status: "active" }
3763
+ }, stepResult ? { $push: { stepResults: stepResult } } : {}, ctx);
3764
+ }
3765
+ async pause(id, ctx) {
3766
+ const doc = await this._transition(id, "paused", ctx);
3767
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ENROLLMENT_PAUSED, { enrollmentId: id }, ctx));
3768
+ return doc;
3769
+ }
3770
+ async resume(id, ctx) {
3771
+ const doc = await this._transition(id, "active", ctx);
3772
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ENROLLMENT_RESUMED, { enrollmentId: id }, ctx));
3773
+ return doc;
3774
+ }
3775
+ async complete(id, ctx) {
3776
+ const doc = await this._transition(id, "completed", ctx, { completedAt: /* @__PURE__ */ new Date() });
3777
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ENROLLMENT_COMPLETED, { enrollmentId: id }, ctx));
3778
+ return doc;
3779
+ }
3780
+ async exit(id, exitReason, ctx) {
3781
+ const existing = await this.getById(id, ctx);
3782
+ if (!existing) throw new NotFoundError("Enrollment", id);
3783
+ const from = existing.status;
3784
+ ENROLLMENT_MACHINE.assertTransition(id, from, "exited");
3785
+ const entry = {
3786
+ status: "exited",
3787
+ occurredAt: /* @__PURE__ */ new Date(),
3788
+ ...ctx?.actorId ? { by: String(ctx.actorId) } : {}
3789
+ };
3790
+ const updated = await assertAndClaim(ENROLLMENT_MACHINE, this, id, {
3791
+ from,
3792
+ to: "exited",
3793
+ patch: {
3794
+ exitReason,
3795
+ statusHistory: [...existing.statusHistory ?? [], entry]
3796
+ },
3797
+ options: ctx
3798
+ });
3799
+ if (!updated) throw new NotFoundError("Enrollment", id);
3800
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ENROLLMENT_EXITED, {
3801
+ enrollmentId: id,
3802
+ exitReason
3803
+ }, ctx));
3804
+ return updated;
3805
+ }
3806
+ /**
3807
+ * Reply detection: exit every ACTIVE enrollment of the given subjects
3808
+ * with reason `'reply'` — the one method a host's
3809
+ * `crm:message.received` subscriber calls:
3810
+ *
3811
+ * ```ts
3812
+ * crm.events.subscribe('crm:message.received', async (e) => {
3813
+ * await crm.repositories.enrollment.exitActiveOnReply(
3814
+ * [{ subjectKind: 'contact', subjectId: e.payload.participantId }],
3815
+ * { organizationId: e.meta.organizationId },
3816
+ * );
3817
+ * });
3818
+ * ```
3819
+ *
3820
+ * Sequences are batch-loaded in ONE `$in` query; enrollments whose
3821
+ * sequence has `replyDetectionEnabled === false` are skipped (a missing
3822
+ * sequence — e.g. deleted — does NOT skip: the default is to exit).
3823
+ * Each exit goes through `exit()` (state-machine CAS + statusHistory +
3824
+ * ENROLLMENT_EXITED event). An enrollment that a concurrent transition
3825
+ * already moved out of `active` loses the CAS and is skipped — the
3826
+ * desired end state ("not active") already holds.
3827
+ *
3828
+ * Returns the enrollments this call actually exited.
3829
+ */
3830
+ async exitActiveOnReply(subjects, ctx) {
3831
+ if (subjects.length === 0) return [];
3832
+ const active = await this.findAll({
3833
+ status: "active",
3834
+ $or: subjects.map((s) => ({
3835
+ subjectKind: s.subjectKind,
3836
+ subjectId: s.subjectId
3837
+ }))
3838
+ }, ctx);
3839
+ if (active.length === 0) return [];
3840
+ const sequenceIds = [...new Set(active.map((e) => String(e.sequenceId)))];
3841
+ const sequences = await this.sequences.findAll({ _id: { $in: sequenceIds } }, ctx);
3842
+ const sequencesById = new Map(sequences.map((s) => [String(s._id), s]));
3843
+ const exited = [];
3844
+ for (const enrollment of active) {
3845
+ if (sequencesById.get(String(enrollment.sequenceId))?.replyDetectionEnabled === false) continue;
3846
+ try {
3847
+ exited.push(await this.exit(String(enrollment._id), "reply", ctx));
3848
+ } catch (err) {
3849
+ if (err instanceof IllegalTransitionError || err instanceof InvalidEnrollmentTransitionError || err instanceof NotFoundError) continue;
3850
+ throw err;
3851
+ }
3852
+ }
3853
+ return exited;
3854
+ }
3855
+ async _transition(id, target, ctx, patch = {}) {
3856
+ const existing = await this.getById(id, ctx);
3857
+ if (!existing) throw new NotFoundError("Enrollment", id);
3858
+ ENROLLMENT_MACHINE.assertTransition(id, existing.status, target);
3859
+ const entry = {
3860
+ status: target,
3861
+ occurredAt: /* @__PURE__ */ new Date(),
3862
+ ...ctx?.actorId ? { by: String(ctx.actorId) } : {}
3863
+ };
3864
+ const updated = await assertAndClaim(ENROLLMENT_MACHINE, this, id, {
3865
+ from: existing.status,
3866
+ to: target,
3867
+ patch: {
3868
+ ...patch,
3869
+ statusHistory: [...existing.statusHistory ?? [], entry]
3870
+ },
3871
+ options: ctx
3872
+ });
3873
+ if (!updated) throw new InvalidEnrollmentTransitionError({
3874
+ from: (await this.getById(id, ctx))?.status ?? existing.status,
3875
+ to: target,
3876
+ enrollmentId: id
3877
+ });
3878
+ return updated;
3879
+ }
3880
+ };
3881
+ //#endregion
3194
3882
  //#region src/repositories/create-repositories.ts
3195
3883
  /**
3196
3884
  * Repository factory — composes the shared mongokit plugin stack and
@@ -3224,6 +3912,7 @@ function createCrmRepositories(models, deps) {
3224
3912
  const account = new AccountRepository(models.Account, plugins, { events }, pagination);
3225
3913
  const campaign = new CampaignRepository(models.Campaign, plugins, { events }, pagination);
3226
3914
  const subscription = new SubscriptionRepository(models.Subscription, plugins, { events }, pagination);
3915
+ const suppression = new SuppressionRepository(models.Suppression, plugins, { events }, pagination);
3227
3916
  const activity = new ActivityRepository(models.Activity, plugins, { events }, pagination);
3228
3917
  const note = new NoteRepository(models.Note, plugins, { events }, pagination);
3229
3918
  const lossReason = new LossReasonRepository(models.LossReason, plugins, { events }, pagination);
@@ -3233,6 +3922,11 @@ function createCrmRepositories(models, deps) {
3233
3922
  events,
3234
3923
  conversations: conversation
3235
3924
  }, pagination);
3925
+ const sequence = new SequenceRepository(models.Sequence, plugins, { events }, pagination);
3926
+ const enrollment = new EnrollmentRepository(models.Enrollment, plugins, {
3927
+ events,
3928
+ sequences: sequence
3929
+ }, pagination);
3236
3930
  return Object.freeze({
3237
3931
  pipeline,
3238
3932
  lead,
@@ -3241,15 +3935,90 @@ function createCrmRepositories(models, deps) {
3241
3935
  account,
3242
3936
  campaign,
3243
3937
  subscription,
3938
+ suppression,
3244
3939
  activity,
3245
3940
  note,
3246
3941
  lossReason,
3247
3942
  emailEvent,
3248
3943
  conversation,
3249
- message
3944
+ message,
3945
+ sequence,
3946
+ enrollment
3250
3947
  });
3251
3948
  }
3252
3949
  //#endregion
3950
+ //#region src/events/dispatch.ts
3951
+ /**
3952
+ * Context key for the post-commit event queue. `Symbol.for` so the host
3953
+ * and the package agree on the key even across duplicated installs.
3954
+ */
3955
+ const PENDING_EVENTS = Symbol.for("@classytic/crm:pendingEvents");
3956
+ /**
3957
+ * EventTransport-compatible wrapper the engine hands to repositories.
3958
+ * `publish`/`subscribe` delegate straight to the underlying transport
3959
+ * (host-facing surface unchanged); domain verbs call `dispatch(ctx, event)`
3960
+ * so session/outbox/queue routing happens in ONE place.
3961
+ */
3962
+ var CrmEventDispatcher = class {
3963
+ transport;
3964
+ outbox;
3965
+ logger;
3966
+ constructor(options) {
3967
+ this.transport = options.transport;
3968
+ this.outbox = options.outbox;
3969
+ this.logger = options.logger;
3970
+ }
3971
+ get name() {
3972
+ return this.transport.name;
3973
+ }
3974
+ /** Raw immediate publish — post-commit flushes and host-side use only. */
3975
+ publish(event) {
3976
+ return this.transport.publish(event);
3977
+ }
3978
+ subscribe(pattern, handler) {
3979
+ return this.transport.subscribe(pattern, handler);
3980
+ }
3981
+ async close() {
3982
+ await this.transport.close?.();
3983
+ }
3984
+ /**
3985
+ * Session-bound dispatch (see module doc for the full contract).
3986
+ * `ctx` comes first so call sites read `dispatch(ctx, createEvent(...))`.
3987
+ */
3988
+ async dispatch(ctx, event) {
3989
+ const pending = ctx?.[PENDING_EVENTS];
3990
+ const session = ctx?.session;
3991
+ if (this.outbox) {
3992
+ const saveOptions = {};
3993
+ if (session !== void 0) saveOptions.session = session;
3994
+ await this.outbox.save(event, saveOptions);
3995
+ } else if (session && !pending) throw new UnmanagedSessionError(event.type);
3996
+ if (pending) {
3997
+ pending.push(event);
3998
+ return;
3999
+ }
4000
+ if (!session) try {
4001
+ await this.transport.publish(event);
4002
+ } catch (err) {
4003
+ this.logger?.error?.(`[crm] publish failed for ${event.type}`, { err });
4004
+ }
4005
+ }
4006
+ };
4007
+ /**
4008
+ * Drain a `[PENDING_EVENTS]` queue through a transport after commit,
4009
+ * swallowing per-event errors (durable delivery is the outbox's job).
4010
+ * Hosts composing their own outer transaction call this after commit;
4011
+ * most should use `engine.withTransaction(ctx, fn)` instead.
4012
+ */
4013
+ async function flushPending(transport, events, logger) {
4014
+ if (!transport || events.length === 0) return;
4015
+ for (const event of events) try {
4016
+ await transport.publish(event);
4017
+ } catch (err) {
4018
+ (logger?.error ?? console.error)(`[crm] publish failed for ${event.type}`, { err });
4019
+ }
4020
+ }
4021
+ //#endregion
3253
4022
  //#region src/events/in-process-bus.ts
3254
4023
  /**
3255
4024
  * In-process event bus — default fallback when no `eventTransport` is
@@ -3305,6 +4074,42 @@ function assertCrmCapabilities(probe, options = {}) {
3305
4074
  }
3306
4075
  }
3307
4076
  //#endregion
4077
+ //#region src/engine/run-unit.ts
4078
+ /**
4079
+ * runUnit — transaction + post-commit-publish discipline for CRM verbs
4080
+ * composed into one atomic unit (the canonical case: `LeadService.convert`
4081
+ * = Account + Contact + Opportunity + lead transition). Same contract as
4082
+ * `@classytic/refit` / `@classytic/review` / `@classytic/cart` /
4083
+ * `@classytic/invoice` (PACKAGE_RULES §P8.1):
4084
+ *
4085
+ * 1. Events dispatched inside the unit are QUEUED (`ctx[PENDING_EVENTS]`)
4086
+ * and flushed only after the transaction commits (queue resets on
4087
+ * retry attempts) — a rollback can't leak ghost events to in-process
4088
+ * subscribers. Outbox rows stay inside the transaction — the relay
4089
+ * reads only committed rows.
4090
+ * 2. A caller-owned `ctx.session` must arrive WITH a queue or an outbox —
4091
+ * `dispatch()` throws `UnmanagedSessionError` otherwise.
4092
+ */
4093
+ async function runUnit(config, ctx, body) {
4094
+ const internalCtx = ctx;
4095
+ const externalQueue = internalCtx[PENDING_EVENTS];
4096
+ if (ctx.session) return body(externalQueue ? {
4097
+ ...internalCtx,
4098
+ [PENDING_EVENTS]: externalQueue
4099
+ } : internalCtx);
4100
+ const pending = [];
4101
+ const result = await withTransaction(config.connection, async (session) => {
4102
+ pending.length = 0;
4103
+ return body({
4104
+ ...internalCtx,
4105
+ session,
4106
+ [PENDING_EVENTS]: pending
4107
+ });
4108
+ }, config.allowNonTransactional ? { allowFallback: true } : {});
4109
+ await flushPending(config.events, pending, config.logger);
4110
+ return result;
4111
+ }
4112
+ //#endregion
3308
4113
  //#region src/engine/create-crm.ts
3309
4114
  async function createCrm(config) {
3310
4115
  if (!config.connection) throw new Error("[crm] CrmConfig.connection is required");
@@ -3318,7 +4123,11 @@ async function createCrm(config) {
3318
4123
  ...config.pagination !== void 0 ? { pagination: config.pagination } : {},
3319
4124
  plugins: config.plugins ?? []
3320
4125
  };
3321
- const events = config.eventTransport ?? new InProcessCrmBus(config.logger ? { logger: config.logger } : void 0);
4126
+ const events = new CrmEventDispatcher({
4127
+ transport: config.eventTransport ?? new InProcessCrmBus(config.logger ? { logger: config.logger } : void 0),
4128
+ outbox: config.outbox,
4129
+ logger: config.logger
4130
+ });
3322
4131
  const models = createCrmModels(config.connection, resolved);
3323
4132
  const repositories = createCrmRepositories(models, {
3324
4133
  events,
@@ -3333,11 +4142,19 @@ async function createCrm(config) {
3333
4142
  repositories,
3334
4143
  events,
3335
4144
  config: Object.freeze(resolved),
4145
+ withTransaction(ctx, body) {
4146
+ return runUnit({
4147
+ connection: config.connection,
4148
+ events,
4149
+ logger: config.logger,
4150
+ allowNonTransactional: config.allowNonTransactional ?? false
4151
+ }, ctx, body);
4152
+ },
3336
4153
  async syncIndexes() {
3337
4154
  await Promise.all(Object.values(models).map((m) => m.syncIndexes()));
3338
4155
  },
3339
4156
  async dispose() {
3340
- await events.close?.();
4157
+ await events.close();
3341
4158
  }
3342
4159
  });
3343
4160
  }
@@ -3429,7 +4246,7 @@ var LeadService = class LeadService {
3429
4246
  opportunityId: String(opportunity._id),
3430
4247
  ...input.by !== void 0 ? { by: input.by } : {}
3431
4248
  }, ctx);
3432
- await this.events.publish(createEvent(CRM_EVENTS.LEAD_CONVERTED, {
4249
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.LEAD_CONVERTED, {
3433
4250
  leadId,
3434
4251
  contactId: String(contact._id),
3435
4252
  accountId: account ? String(account._id) : void 0,
@@ -3954,6 +4771,22 @@ const SubscriptionComplained = defineCrmEvent({
3954
4771
  campaignId: z.string()
3955
4772
  })
3956
4773
  });
4774
+ const SuppressionAdded = defineCrmEvent({
4775
+ name: CRM_EVENTS.SUPPRESSION_ADDED,
4776
+ resource: "suppression",
4777
+ description: "suppress() inserted a NEW row on the org-wide suppression list (idempotent re-suppress of an existing email emits nothing). organizationId rides event meta.",
4778
+ zodSchema: z.object({
4779
+ email: z.string(),
4780
+ reason: z.enum(SUPPRESSION_REASONS),
4781
+ source: z.string().optional()
4782
+ })
4783
+ });
4784
+ const SuppressionRemoved = defineCrmEvent({
4785
+ name: CRM_EVENTS.SUPPRESSION_REMOVED,
4786
+ resource: "suppression",
4787
+ description: "unsuppress() removed an email from the org-wide suppression list.",
4788
+ zodSchema: z.object({ email: z.string() })
4789
+ });
3957
4790
  const emailEventSchema = z.object({
3958
4791
  eventId: z.string(),
3959
4792
  kind: z.string(),
@@ -4054,6 +4887,82 @@ const MessageSent = defineCrmEvent({
4054
4887
  externalMessageId: z.string().optional()
4055
4888
  })
4056
4889
  });
4890
+ const SequenceCreated = defineCrmEvent({
4891
+ name: CRM_EVENTS.SEQUENCE_CREATED,
4892
+ resource: "sequence",
4893
+ description: "A sequence draft was created. Emitted by the HOST after repo.create() (the package ships no createSequence verb) — defined here so registries validate the payload.",
4894
+ zodSchema: z.object({
4895
+ sequenceId: z.string(),
4896
+ name: z.string().optional()
4897
+ })
4898
+ });
4899
+ const SequenceActivated = defineCrmEvent({
4900
+ name: CRM_EVENTS.SEQUENCE_ACTIVATED,
4901
+ resource: "sequence",
4902
+ description: "activate() moved a sequence draft/paused → active.",
4903
+ zodSchema: z.object({ sequenceId: z.string() })
4904
+ });
4905
+ const SequencePaused = defineCrmEvent({
4906
+ name: CRM_EVENTS.SEQUENCE_PAUSED,
4907
+ resource: "sequence",
4908
+ description: "pause() suspended an active sequence (existing enrollments hold).",
4909
+ zodSchema: z.object({ sequenceId: z.string() })
4910
+ });
4911
+ const SequenceArchived = defineCrmEvent({
4912
+ name: CRM_EVENTS.SEQUENCE_ARCHIVED,
4913
+ resource: "sequence",
4914
+ description: "archive() retired a sequence — no new enrollments.",
4915
+ zodSchema: z.object({ sequenceId: z.string() })
4916
+ });
4917
+ const EnrollmentCreated = defineCrmEvent({
4918
+ name: CRM_EVENTS.ENROLLMENT_CREATED,
4919
+ resource: "enrollment",
4920
+ description: "enroll() attached a subject to a sequence.",
4921
+ zodSchema: z.object({
4922
+ enrollmentId: z.string(),
4923
+ sequenceId: z.string(),
4924
+ subjectId: z.string(),
4925
+ subjectKind: z.string()
4926
+ })
4927
+ });
4928
+ const EnrollmentStepSent = defineCrmEvent({
4929
+ name: CRM_EVENTS.ENROLLMENT_STEP_SENT,
4930
+ resource: "enrollment",
4931
+ description: "A sequence step was delivered. Emitted by the HOST outreach layer after its send adapter succeeds + advanceStep() commits (Buffer-style pattern in CLAUDE.md) — defined here so registries validate the payload.",
4932
+ zodSchema: z.object({
4933
+ enrollmentId: z.string(),
4934
+ sequenceId: z.string().optional(),
4935
+ stepIndex: z.number(),
4936
+ channel: z.string().optional()
4937
+ })
4938
+ });
4939
+ const EnrollmentPaused = defineCrmEvent({
4940
+ name: CRM_EVENTS.ENROLLMENT_PAUSED,
4941
+ resource: "enrollment",
4942
+ description: "pause() held an active enrollment.",
4943
+ zodSchema: z.object({ enrollmentId: z.string() })
4944
+ });
4945
+ const EnrollmentResumed = defineCrmEvent({
4946
+ name: CRM_EVENTS.ENROLLMENT_RESUMED,
4947
+ resource: "enrollment",
4948
+ description: "resume() reactivated a paused enrollment.",
4949
+ zodSchema: z.object({ enrollmentId: z.string() })
4950
+ });
4951
+ const EnrollmentCompleted = defineCrmEvent({
4952
+ name: CRM_EVENTS.ENROLLMENT_COMPLETED,
4953
+ resource: "enrollment",
4954
+ description: "complete() finished the final step of an enrollment.",
4955
+ zodSchema: z.object({ enrollmentId: z.string() })
4956
+ });
4957
+ const EnrollmentExited = defineCrmEvent({
4958
+ name: CRM_EVENTS.ENROLLMENT_EXITED,
4959
+ resource: "enrollment",
4960
+ description: "exit() removed a subject before completion (reply/unsubscribe/manual/goal).",
4961
+ zodSchema: z.object({
4962
+ enrollmentId: z.string(),
4963
+ exitReason: z.string().optional()
4964
+ })
4965
+ });
4057
4966
  const crmEventDefinitions = [
4058
4967
  LeadCreated,
4059
4968
  LeadStatusChanged,
@@ -4100,6 +5009,8 @@ const crmEventDefinitions = [
4100
5009
  SubscriptionUnsubscribed,
4101
5010
  SubscriptionBounced,
4102
5011
  SubscriptionComplained,
5012
+ SuppressionAdded,
5013
+ SuppressionRemoved,
4103
5014
  EmailEventOpened,
4104
5015
  EmailEventClicked,
4105
5016
  EmailEventBounced,
@@ -4110,17 +5021,34 @@ const crmEventDefinitions = [
4110
5021
  ConversationResolved,
4111
5022
  ConversationReopened,
4112
5023
  MessageReceived,
4113
- MessageSent
5024
+ MessageSent,
5025
+ SequenceCreated,
5026
+ SequenceActivated,
5027
+ SequencePaused,
5028
+ SequenceArchived,
5029
+ EnrollmentCreated,
5030
+ EnrollmentStepSent,
5031
+ EnrollmentPaused,
5032
+ EnrollmentResumed,
5033
+ EnrollmentCompleted,
5034
+ EnrollmentExited
4114
5035
  ];
4115
5036
  //#endregion
4116
5037
  //#region src/shared/calculators/forecast.ts
4117
5038
  /**
5039
+ * Pure forecast calculators — no I/O, no Mongo. Hosts call these after
5040
+ * loading opportunities via `repositories.opportunity.list(...)`.
5041
+ *
5042
+ * Amount is flat (`amount` Number + `currency` String per PACKAGE_RULES
5043
+ * §4) — same shape the opportunity schema stores.
5044
+ */
5045
+ /**
4118
5046
  * Aggregate opportunities into a forecast table — one row per bucket.
4119
5047
  * Currency mismatch within a bucket throws — partition by currency
4120
5048
  * upstream when multi-currency.
4121
5049
  */
4122
5050
  function forecast(opportunities, options = {}) {
4123
- const bucketFn = options.bucket ?? defaultBucket;
5051
+ const bucketFn = options.bucket ?? (options.timeZone !== void 0 ? zoneBucket(options.timeZone) : defaultBucket);
4124
5052
  const include = options.include ?? defaultInclude;
4125
5053
  const rows = /* @__PURE__ */ new Map();
4126
5054
  for (const o of opportunities) {
@@ -4153,6 +5081,10 @@ function defaultBucket(o) {
4153
5081
  const d = o.expectedCloseAt;
4154
5082
  return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}`;
4155
5083
  }
5084
+ /** Month bucket cut at the business's local midnight, not UTC. */
5085
+ function zoneBucket(timeZone) {
5086
+ return (o) => civilDateOf(o.expectedCloseAt, timeZone).slice(0, 7);
5087
+ }
4156
5088
  /**
4157
5089
  * "Where do deals die?" summary — opportunities partitioned across
4158
5090
  * stages with current count + won/lost-from-here.
@@ -4189,7 +5121,7 @@ const objectIdLike = z.union([z.string().min(1), z.string()]);
4189
5121
  const metadataSchema = z.record(z.string(), z.unknown()).optional();
4190
5122
  const tagsSchema = z.array(z.string().min(1).max(64)).max(50).optional();
4191
5123
  const moneyAmountSchema = z.number().int().min(0).max(Number.MAX_SAFE_INTEGER);
4192
- const currencyCodeSchema = z.string().regex(/^[A-Z]{3}$/, "Currency must be a 3-letter ISO code");
5124
+ const currencyCodeSchema = z.string().regex(CURRENCY_PATTERN, "currency must be an ISO 4217 code (3 uppercase letters)");
4193
5125
  //#endregion
4194
5126
  //#region src/validators/pipeline.schema.ts
4195
5127
  const stageInputSchema = z.object({
@@ -4243,7 +5175,7 @@ const convertLeadSchema = z.object({
4243
5175
  pipelineId: objectIdLike,
4244
5176
  opportunityName: z.string().min(1).max(200).optional(),
4245
5177
  amount: z.number().nonnegative().optional(),
4246
- currency: z.string().regex(/^[A-Z]{3}$/).optional(),
5178
+ currency: currencyCodeSchema.optional(),
4247
5179
  expectedCloseAt: z.coerce.date().optional(),
4248
5180
  by: z.string().optional()
4249
5181
  });
@@ -4399,6 +5331,21 @@ const subscriptionFilterSchema = z.object({
4399
5331
  campaignId: objectIdLike.optional()
4400
5332
  });
4401
5333
  //#endregion
5334
+ //#region src/validators/suppression.schema.ts
5335
+ const suppressSchema = z.object({
5336
+ email: z.string().email().max(320),
5337
+ reason: z.enum(SUPPRESSION_REASONS),
5338
+ source: z.string().max(200).optional(),
5339
+ subjectKind: z.string().max(50).optional(),
5340
+ subjectId: z.string().max(100).optional(),
5341
+ metadata: metadataSchema
5342
+ });
5343
+ const unsuppressSchema = z.object({ email: z.string().email().max(320) });
5344
+ const suppressionFilterSchema = z.object({
5345
+ email: z.string().max(320).optional(),
5346
+ reason: z.enum(SUPPRESSION_REASONS).optional()
5347
+ });
5348
+ //#endregion
4402
5349
  //#region src/validators/activity.schema.ts
4403
5350
  const planActivitySchema = z.object({
4404
5351
  type: z.enum(ACTIVITY_TYPES),
@@ -4475,6 +5422,54 @@ const emailEventFilterSchema = z.object({
4475
5422
  messageId: z.string().optional()
4476
5423
  });
4477
5424
  //#endregion
5425
+ //#region src/validators/sequence.schema.ts
5426
+ const sendEmailStepSchema = z.object({
5427
+ id: z.string().min(1),
5428
+ type: z.literal("SEND_EMAIL"),
5429
+ name: z.string().max(100).optional(),
5430
+ subject: z.string().min(1).max(300),
5431
+ htmlBody: z.string().min(1),
5432
+ fromName: z.string().max(100).optional(),
5433
+ fromEmail: z.string().email().optional()
5434
+ });
5435
+ const delayStepSchema = z.object({
5436
+ id: z.string().min(1),
5437
+ type: z.literal("DELAY"),
5438
+ name: z.string().max(100).optional(),
5439
+ settings: z.object({
5440
+ days: z.number().int().min(0).optional(),
5441
+ hours: z.number().int().min(0).optional(),
5442
+ minutes: z.number().int().min(0).optional()
5443
+ })
5444
+ });
5445
+ const sequenceStepSchema = z.discriminatedUnion("type", [sendEmailStepSchema, delayStepSchema]);
5446
+ const createSequenceSchema = z.object({
5447
+ name: z.string().min(1).max(200),
5448
+ description: z.string().max(1e3).optional(),
5449
+ fromName: z.string().max(100).optional(),
5450
+ fromEmail: z.string().email().optional(),
5451
+ steps: z.array(sequenceStepSchema).default([]),
5452
+ replyDetectionEnabled: z.boolean().default(true),
5453
+ tags: tagsSchema,
5454
+ ownerId: z.string().optional(),
5455
+ metadata: metadataSchema
5456
+ });
5457
+ const updateSequenceSchema = createSequenceSchema.partial();
5458
+ const sequenceFilterSchema = z.object({
5459
+ status: z.enum([
5460
+ "draft",
5461
+ "active",
5462
+ "paused",
5463
+ "archived"
5464
+ ]).optional(),
5465
+ ownerId: z.string().optional(),
5466
+ tags: z.string().optional(),
5467
+ search: z.string().optional(),
5468
+ page: z.coerce.number().int().min(1).optional(),
5469
+ limit: z.coerce.number().int().min(1).max(200).optional(),
5470
+ sort: z.string().optional()
5471
+ });
5472
+ //#endregion
4478
5473
  //#region src/validators/index.ts
4479
5474
  var validators_exports = /* @__PURE__ */ __exportAll({
4480
5475
  accountFilterSchema: () => accountFilterSchema,
@@ -4494,6 +5489,7 @@ var validators_exports = /* @__PURE__ */ __exportAll({
4494
5489
  createLeadSchema: () => createLeadSchema,
4495
5490
  createLossReasonSchema: () => createLossReasonSchema,
4496
5491
  createPipelineSchema: () => createPipelineSchema,
5492
+ createSequenceSchema: () => createSequenceSchema,
4497
5493
  currencyCodeSchema: () => currencyCodeSchema,
4498
5494
  emailEventFilterSchema: () => emailEventFilterSchema,
4499
5495
  leadFilterSchema: () => leadFilterSchema,
@@ -4508,11 +5504,16 @@ var validators_exports = /* @__PURE__ */ __exportAll({
4508
5504
  pipelineFilterSchema: () => pipelineFilterSchema,
4509
5505
  planActivitySchema: () => planActivitySchema,
4510
5506
  recordEmailEventSchema: () => recordEmailEventSchema,
5507
+ sequenceFilterSchema: () => sequenceFilterSchema,
5508
+ sequenceStepSchema: () => sequenceStepSchema,
4511
5509
  stageInputSchemaExport: () => stageInputSchemaExport,
4512
5510
  subscribeSchema: () => subscribeSchema,
4513
5511
  subscriptionFilterSchema: () => subscriptionFilterSchema,
5512
+ suppressSchema: () => suppressSchema,
5513
+ suppressionFilterSchema: () => suppressionFilterSchema,
4514
5514
  tagsSchema: () => tagsSchema,
4515
5515
  unsubscribeSchema: () => unsubscribeSchema,
5516
+ unsuppressSchema: () => unsuppressSchema,
4516
5517
  updateAccountSchema: () => updateAccountSchema,
4517
5518
  updateActivitySchema: () => updateActivitySchema,
4518
5519
  updateCampaignSchema: () => updateCampaignSchema,
@@ -4520,7 +5521,8 @@ var validators_exports = /* @__PURE__ */ __exportAll({
4520
5521
  updateLeadSchema: () => updateLeadSchema,
4521
5522
  updateLossReasonSchema: () => updateLossReasonSchema,
4522
5523
  updateNoteSchema: () => updateNoteSchema,
4523
- updatePipelineSchema: () => updatePipelineSchema
5524
+ updatePipelineSchema: () => updatePipelineSchema,
5525
+ updateSequenceSchema: () => updateSequenceSchema
4524
5526
  });
4525
5527
  //#endregion
4526
- export { ACTIVITY_MACHINE, ACTIVITY_STATUSES, ACTIVITY_TRANSITIONS, ACTIVITY_TYPES, AccountAssigned, AccountCreated, AccountEnriched, AccountMerged, AccountRepository, AccountUpdated, ActivityCancelled, ActivityCompleted, ActivityOutcomeRecorded, ActivityPlanned, ActivityRepository, ActivityRescheduled, CAMPAIGN_MACHINE, CAMPAIGN_STATUSES, CAMPAIGN_TRANSITIONS, CONVERSATION_CHANNELS, CONVERSATION_PARTICIPANT_KINDS, CONVERSATION_STATUSES, CRM_EVENTS, CRM_MODEL_NAMES, CampaignActivated, CampaignArchived, CampaignCompleted, CampaignCreated, CampaignPaused, CampaignRepository, CampaignResumed, ContactAssigned, ContactCreated, ContactEnriched, ContactMerged, ContactRepository, ContactUpdated, ConversationAssigned, ConversationCreated, ConversationReopened, ConversationRepository, ConversationResolved, CrmError, DuplicateCampaignError, DuplicateLeadError, DuplicateSubscriptionError, EMAIL_EVENT_KINDS, EMAIL_EVENT_SUBJECT_KINDS, EmailEventBounced, EmailEventClicked, EmailEventComplained, EmailEventOpened, EmailEventRepository, EmailEventUnsubscribed, EmptyPipelineError, InProcessCrmBus, InvalidActivityTransitionError, InvalidCampaignTransitionError, InvalidConfirmationTokenError, InvalidLeadTransitionError, InvalidOpportunityTransitionError, InvalidSubscriptionTransitionError, InvalidUnsubscribeTokenError, LEAD_MACHINE, LEAD_STATUSES, LEAD_TRANSITIONS, LeadAlreadyConvertedError, LeadAssigned, LeadConverted, LeadCreated, LeadDisqualified, LeadEnriched, LeadRepository, LeadScored, LeadService, LeadStatusChanged, LossReasonRepository, MESSAGE_DELIVERY_STATUSES, MESSAGE_DIRECTIONS, MessageReceived, MessageRepository, MessageSent, NotFoundError, NoteAdded, NoteRepository, OPPORTUNITY_MACHINE, OPPORTUNITY_STATUSES, OPPORTUNITY_TRANSITIONS, OpportunityAbandoned, OpportunityAmountChanged, OpportunityAssigned, OpportunityCreated, OpportunityExtended, OpportunityLost, OpportunityRepository, OpportunityStageAdvanced, OpportunityStageRegressed, OpportunityWon, PipelineArchived, PipelineCreated, PipelineRepository, SUBJECT_KINDS, SUBSCRIPTION_MACHINE, SUBSCRIPTION_STATUSES, SUBSCRIPTION_TRANSITIONS, StageNotInPipelineError, SubscriptionBounced, SubscriptionComplained, SubscriptionConfirmed, SubscriptionCreated, SubscriptionRepository, SubscriptionUnsubscribed, appendStatus, buildAccountSchema, buildActivitySchema, buildCampaignSchema, buildContactSchema, buildConversationSchema, buildEmailEventSchema, buildLeadSchema, buildLossReasonSchema, buildMessageSchema, buildNoteSchema, buildOpportunitySchema, buildPipelineSchema, buildSubscriptionSchema, canTransitionActivity, canTransitionCampaign, canTransitionLead, canTransitionOpportunity, canTransitionSubscription, composeScorers, createAccountModel, createActivityModel, createCampaignModel, createContactModel, createConversationModel, createCrm, createCrmModels, createCrmRepositories, createEmailEventModel, createEvent, createLeadModel, createLossReasonModel, createMessageModel, createNoteModel, createOpportunityModel, createPipelineModel, createSubscriptionModel, crmEventDefinitions, defaultLeadScorer, forecast, lastTransitionTo, latestEntry, pipelineConversion, tenantFieldDef, timeInStatus, validators_exports as validators };
5528
+ export { ACTIVITY_MACHINE, ACTIVITY_STATUSES, ACTIVITY_TRANSITIONS, ACTIVITY_TYPES, AccountAssigned, AccountCreated, AccountEnriched, AccountMerged, AccountRepository, AccountUpdated, ActivityCancelled, ActivityCompleted, ActivityOutcomeRecorded, ActivityPlanned, ActivityRepository, ActivityRescheduled, AlreadyEnrolledError, CAMPAIGN_MACHINE, CAMPAIGN_STATUSES, CAMPAIGN_TRANSITIONS, CONVERSATION_CHANNELS, CONVERSATION_PARTICIPANT_KINDS, CONVERSATION_STATUSES, CRM_EVENTS, CRM_MODEL_NAMES, CampaignActivated, CampaignArchived, CampaignCompleted, CampaignCreated, CampaignPaused, CampaignRepository, CampaignResumed, ContactAssigned, ContactCreated, ContactEnriched, ContactMerged, ContactRepository, ContactUpdated, ConversationAssigned, ConversationCreated, ConversationReopened, ConversationRepository, ConversationResolved, CrmError, CrmEventDispatcher, DuplicateCampaignError, DuplicateLeadError, DuplicateSubscriptionError, EMAIL_EVENT_KINDS, EMAIL_EVENT_SUBJECT_KINDS, ENROLLMENT_MACHINE, ENROLLMENT_STATUSES, ENROLLMENT_SUBJECT_KINDS, ENROLLMENT_TRANSITIONS, EVENT_DERIVED_SUPPRESSION_REASONS, EmailEventBounced, EmailEventClicked, EmailEventComplained, EmailEventOpened, EmailEventRepository, EmailEventUnsubscribed, EmptyPipelineError, EnrollmentCompleted, EnrollmentCreated, EnrollmentExited, EnrollmentPaused, EnrollmentRepository, EnrollmentResumed, EnrollmentStepSent, InProcessCrmBus, InvalidActivityTransitionError, InvalidCampaignTransitionError, InvalidConfirmationTokenError, InvalidEnrollmentTransitionError, InvalidLeadTransitionError, InvalidOpportunityTransitionError, InvalidSequenceTransitionError, InvalidSubscriptionTransitionError, InvalidUnsubscribeTokenError, LEAD_MACHINE, LEAD_STATUSES, LEAD_TRANSITIONS, LeadAlreadyConvertedError, LeadAssigned, LeadConverted, LeadCreated, LeadDisqualified, LeadEnriched, LeadRepository, LeadScored, LeadService, LeadStatusChanged, LossReasonRepository, MESSAGE_DELIVERY_STATUSES, MESSAGE_DIRECTIONS, MessageReceived, MessageRepository, MessageSent, NotFoundError, NoteAdded, NoteRepository, OPPORTUNITY_MACHINE, OPPORTUNITY_STATUSES, OPPORTUNITY_TRANSITIONS, OpportunityAbandoned, OpportunityAmountChanged, OpportunityAssigned, OpportunityCreated, OpportunityExtended, OpportunityLost, OpportunityRepository, OpportunityStageAdvanced, OpportunityStageRegressed, OpportunityWon, PENDING_EVENTS, PipelineArchived, PipelineCreated, PipelineRepository, SEQUENCE_MACHINE, SEQUENCE_STATUSES, SEQUENCE_TRANSITIONS, SUBJECT_KINDS, SUBSCRIPTION_MACHINE, SUBSCRIPTION_STATUSES, SUBSCRIPTION_TRANSITIONS, SUPPRESSION_REASONS, SequenceActivated, SequenceArchived, SequenceCreated, SequencePaused, SequenceRepository, StageNotInPipelineError, SubscriptionBounced, SubscriptionComplained, SubscriptionConfirmed, SubscriptionCreated, SubscriptionRepository, SubscriptionUnsubscribed, SuppressionAdded, SuppressionRemoved, SuppressionRepository, UnmanagedSessionError, appendStatus, buildAccountSchema, buildActivitySchema, buildCampaignSchema, buildContactSchema, buildConversationSchema, buildEmailEventSchema, buildEnrollmentSchema, buildLeadSchema, buildLossReasonSchema, buildMessageSchema, buildNoteSchema, buildOpportunitySchema, buildPipelineSchema, buildSequenceSchema, buildSubscriptionSchema, buildSuppressionSchema, canTransitionActivity, canTransitionCampaign, canTransitionEnrollment, canTransitionLead, canTransitionOpportunity, canTransitionSequence, canTransitionSubscription, composeScorers, createAccountModel, createActivityModel, createCampaignModel, createContactModel, createConversationModel, createCrm, createCrmModels, createCrmRepositories, createEmailEventModel, createEvent, createLeadModel, createLossReasonModel, createMessageModel, createNoteModel, createOpportunityModel, createPipelineModel, createSubscriptionModel, crmEventDefinitions, defaultLeadScorer, flushPending, forecast, lastTransitionTo, latestEntry, pipelineConversion, runUnit, tenantFieldDef, timeInStatus, validators_exports as validators };