@classytic/crm 0.5.1 → 0.8.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,
@@ -270,6 +292,25 @@ const statusEntrySchema$3 = new Schema({
270
292
  by: { type: String },
271
293
  note: { type: String }
272
294
  }, { _id: false });
295
+ const engagementSchema = new Schema({
296
+ opens: {
297
+ type: Number,
298
+ default: 0
299
+ },
300
+ clicks: {
301
+ type: Number,
302
+ default: 0
303
+ },
304
+ replies: {
305
+ type: Number,
306
+ default: 0
307
+ },
308
+ activities: {
309
+ type: Number,
310
+ default: 0
311
+ },
312
+ lastEngagedAt: { type: Date }
313
+ }, { _id: false });
273
314
  function buildLeadSchema(config) {
274
315
  const schema = new Schema({
275
316
  organizationId: tenantFieldDef(config),
@@ -324,6 +365,10 @@ function buildLeadSchema(config) {
324
365
  type: Number,
325
366
  default: 0
326
367
  },
368
+ engagement: {
369
+ type: engagementSchema,
370
+ default: () => ({})
371
+ },
327
372
  status: {
328
373
  type: String,
329
374
  enum: LEAD_STATUSES,
@@ -331,7 +376,7 @@ function buildLeadSchema(config) {
331
376
  index: true
332
377
  },
333
378
  statusHistory: {
334
- type: [statusEntrySchema$3],
379
+ type: [statusEntrySchema$5],
335
380
  default: []
336
381
  },
337
382
  ownerId: {
@@ -418,7 +463,7 @@ function canTransitionOpportunity(from, to) {
418
463
  * Money amount lives flat (`amount` + `currency`) so `$sum` aggregates work; the repo
419
464
  * surface returns it that way too (no Money translation layer per PACKAGE_RULES §4).
420
465
  */
421
- const statusEntrySchema$2 = new Schema({
466
+ const statusEntrySchema$4 = new Schema({
422
467
  status: {
423
468
  type: String,
424
469
  enum: OPPORTUNITY_STATUSES,
@@ -466,7 +511,7 @@ function buildOpportunitySchema(config) {
466
511
  index: true
467
512
  },
468
513
  statusHistory: {
469
- type: [statusEntrySchema$2],
514
+ type: [statusEntrySchema$4],
470
515
  default: []
471
516
  },
472
517
  amount: { type: Number },
@@ -779,7 +824,7 @@ function canTransitionCampaign(from, to) {
779
824
  * Campaign — a marketing/sales motion targeting a contact list.
780
825
  * Money fields stored flat (amount + currency) for $sum aggregations.
781
826
  */
782
- const statusEntrySchema$1 = new Schema({
827
+ const statusEntrySchema$3 = new Schema({
783
828
  status: {
784
829
  type: String,
785
830
  enum: CAMPAIGN_STATUSES,
@@ -813,7 +858,7 @@ function buildCampaignSchema(config) {
813
858
  index: true
814
859
  },
815
860
  statusHistory: {
816
- type: [statusEntrySchema$1],
861
+ type: [statusEntrySchema$3],
817
862
  default: []
818
863
  },
819
864
  utmSource: {
@@ -928,7 +973,7 @@ function canTransitionSubscription(from, to) {
928
973
  * same connection. TypeScript class + file names retain `Subscription` for
929
974
  * source-level continuity — only the Mongo identity changed in v0.4.0.
930
975
  */
931
- const statusEntrySchema = new Schema({
976
+ const statusEntrySchema$2 = new Schema({
932
977
  status: {
933
978
  type: String,
934
979
  enum: SUBSCRIPTION_STATUSES,
@@ -961,7 +1006,7 @@ function buildSubscriptionSchema(config) {
961
1006
  index: true
962
1007
  },
963
1008
  statusHistory: {
964
- type: [statusEntrySchema],
1009
+ type: [statusEntrySchema$2],
965
1010
  default: []
966
1011
  },
967
1012
  source: {
@@ -1036,6 +1081,83 @@ function createSubscriptionModel(connection, config) {
1036
1081
  return connection.model("EmailSubscription", buildSubscriptionSchema(config));
1037
1082
  }
1038
1083
  //#endregion
1084
+ //#region src/enums/suppression-reason.ts
1085
+ /** Reasons derived 1:1 from an email engagement event kind. */
1086
+ const EVENT_DERIVED_SUPPRESSION_REASONS = [
1087
+ "bounce",
1088
+ "complaint",
1089
+ "unsubscribe"
1090
+ ];
1091
+ const SUPPRESSION_REASONS = [...EVENT_DERIVED_SUPPRESSION_REASONS, "manual"];
1092
+ //#endregion
1093
+ //#region src/models/suppression.schema.ts
1094
+ /**
1095
+ * Suppression — org-wide "never email this address again" list.
1096
+ *
1097
+ * Subscription unsubscribes are per-campaign; a Suppression row is the
1098
+ * GLOBAL gate a host consults before EVERY outbound email (campaign
1099
+ * fanout, sequence step sends, one-off notifications). One row per
1100
+ * (org, email) — `suppress()` is getOrCreate-idempotent against the
1101
+ * unique index below.
1102
+ *
1103
+ * `subjectKind`/`subjectId` are optional String external refs (rule 7 —
1104
+ * polymorphic across Contact/Lead/host-owned models, so no
1105
+ * ObjectId+refPath). `source` is a free-form origin marker, e.g.
1106
+ * `'campaign:<id>'`, `'sequence:<id>'`, `'api'`, `'import'`.
1107
+ */
1108
+ function buildSuppressionSchema(config) {
1109
+ const schema = new Schema({
1110
+ organizationId: tenantFieldDef(config),
1111
+ email: {
1112
+ type: String,
1113
+ required: true,
1114
+ trim: true,
1115
+ lowercase: true,
1116
+ maxlength: 320
1117
+ },
1118
+ reason: {
1119
+ type: String,
1120
+ enum: SUPPRESSION_REASONS,
1121
+ required: true
1122
+ },
1123
+ source: {
1124
+ type: String,
1125
+ trim: true,
1126
+ maxlength: 200
1127
+ },
1128
+ subjectKind: {
1129
+ type: String,
1130
+ trim: true,
1131
+ maxlength: 50
1132
+ },
1133
+ subjectId: {
1134
+ type: String,
1135
+ trim: true,
1136
+ maxlength: 100
1137
+ },
1138
+ suppressedAt: {
1139
+ type: Date,
1140
+ required: true,
1141
+ default: () => /* @__PURE__ */ new Date()
1142
+ },
1143
+ metadata: { type: Schema.Types.Mixed }
1144
+ }, { timestamps: true });
1145
+ schema.index({
1146
+ organizationId: 1,
1147
+ email: 1
1148
+ }, { unique: true });
1149
+ schema.index({
1150
+ organizationId: 1,
1151
+ reason: 1,
1152
+ suppressedAt: -1
1153
+ });
1154
+ schema.index({
1155
+ organizationId: 1,
1156
+ suppressedAt: -1
1157
+ });
1158
+ return schema;
1159
+ }
1160
+ //#endregion
1039
1161
  //#region src/enums/activity.ts
1040
1162
  /**
1041
1163
  * Activity type vocabulary. The package stores `type` as free-form
@@ -1608,6 +1730,263 @@ function createMessageModel(connection, config) {
1608
1730
  return connection.model("Message", buildMessageSchema(config));
1609
1731
  }
1610
1732
  //#endregion
1733
+ //#region src/enums/sequence-status.ts
1734
+ /**
1735
+ * Email sequence lifecycle.
1736
+ *
1737
+ * draft — being built; no enrollments can be created
1738
+ * active — accepting new enrollments; steps are executing
1739
+ * paused — no new enrollments; running enrollments pause at next step
1740
+ * archived — soft-hidden; historical data preserved
1741
+ */
1742
+ const SEQUENCE_STATUSES = [
1743
+ "draft",
1744
+ "active",
1745
+ "paused",
1746
+ "archived"
1747
+ ];
1748
+ const SEQUENCE_TRANSITIONS = {
1749
+ draft: ["active", "archived"],
1750
+ active: ["paused", "archived"],
1751
+ paused: ["active", "archived"],
1752
+ archived: []
1753
+ };
1754
+ function canTransitionSequence(from, to) {
1755
+ return SEQUENCE_TRANSITIONS[from].includes(to);
1756
+ }
1757
+ //#endregion
1758
+ //#region src/models/sequence.schema.ts
1759
+ /**
1760
+ * Sequence — ordered cadence of SEND_EMAIL and DELAY steps.
1761
+ *
1762
+ * Steps are stored as a JSON array (Mixed) so the host can add new step
1763
+ * types without a schema migration. The discriminator is `step.type`.
1764
+ */
1765
+ const statusEntrySchema$1 = new Schema({
1766
+ status: {
1767
+ type: String,
1768
+ enum: SEQUENCE_STATUSES,
1769
+ required: true
1770
+ },
1771
+ occurredAt: {
1772
+ type: Date,
1773
+ required: true
1774
+ },
1775
+ by: { type: String },
1776
+ note: { type: String }
1777
+ }, { _id: false });
1778
+ function buildSequenceSchema(config) {
1779
+ const schema = new Schema({
1780
+ organizationId: tenantFieldDef(config),
1781
+ name: {
1782
+ type: String,
1783
+ required: true,
1784
+ trim: true,
1785
+ maxlength: 200
1786
+ },
1787
+ description: {
1788
+ type: String,
1789
+ trim: true,
1790
+ maxlength: 1e3
1791
+ },
1792
+ status: {
1793
+ type: String,
1794
+ enum: SEQUENCE_STATUSES,
1795
+ default: "draft",
1796
+ required: true
1797
+ },
1798
+ fromName: {
1799
+ type: String,
1800
+ trim: true,
1801
+ maxlength: 100
1802
+ },
1803
+ fromEmail: {
1804
+ type: String,
1805
+ trim: true,
1806
+ maxlength: 200
1807
+ },
1808
+ steps: {
1809
+ type: [Schema.Types.Mixed],
1810
+ default: []
1811
+ },
1812
+ replyDetectionEnabled: {
1813
+ type: Boolean,
1814
+ default: true
1815
+ },
1816
+ tags: {
1817
+ type: [String],
1818
+ default: []
1819
+ },
1820
+ ownerId: { type: String },
1821
+ metadata: { type: Schema.Types.Mixed },
1822
+ statusHistory: {
1823
+ type: [statusEntrySchema$1],
1824
+ default: []
1825
+ }
1826
+ }, { timestamps: true });
1827
+ schema.index({
1828
+ organizationId: 1,
1829
+ status: 1
1830
+ });
1831
+ schema.index({
1832
+ organizationId: 1,
1833
+ createdAt: -1
1834
+ });
1835
+ return schema;
1836
+ }
1837
+ //#endregion
1838
+ //#region src/enums/enrollment-status.ts
1839
+ /**
1840
+ * Enrollment lifecycle.
1841
+ *
1842
+ * active — steps are executing (or waiting on a delay)
1843
+ * paused — execution halted; can be resumed
1844
+ * completed — all steps ran to completion
1845
+ * exited — removed early (reply, unsubscribe, bounce, manual)
1846
+ */
1847
+ const ENROLLMENT_STATUSES = [
1848
+ "active",
1849
+ "paused",
1850
+ "completed",
1851
+ "exited"
1852
+ ];
1853
+ const ENROLLMENT_TRANSITIONS = {
1854
+ active: [
1855
+ "paused",
1856
+ "completed",
1857
+ "exited"
1858
+ ],
1859
+ paused: ["active", "exited"],
1860
+ completed: [],
1861
+ exited: []
1862
+ };
1863
+ function canTransitionEnrollment(from, to) {
1864
+ return ENROLLMENT_TRANSITIONS[from].includes(to);
1865
+ }
1866
+ //#endregion
1867
+ //#region src/models/enrollment.schema.ts
1868
+ /**
1869
+ * Enrollment — a single subject (lead or contact) progressing through a Sequence.
1870
+ *
1871
+ * `currentStepIndex` tracks which step to execute next. The sequence workflow
1872
+ * reads this field at each iteration so crashes / pauses resume from the
1873
+ * correct position without re-running completed steps.
1874
+ */
1875
+ const ENROLLMENT_SUBJECT_KINDS = ["lead", "contact"];
1876
+ const stepResultSchema = new Schema({
1877
+ stepId: {
1878
+ type: String,
1879
+ required: true
1880
+ },
1881
+ stepType: {
1882
+ type: String,
1883
+ required: true
1884
+ },
1885
+ status: {
1886
+ type: String,
1887
+ enum: [
1888
+ "pending",
1889
+ "sent",
1890
+ "failed",
1891
+ "skipped"
1892
+ ],
1893
+ required: true
1894
+ },
1895
+ sentAt: { type: Date },
1896
+ error: { type: String }
1897
+ }, { _id: false });
1898
+ const statusEntrySchema = new Schema({
1899
+ status: {
1900
+ type: String,
1901
+ enum: ENROLLMENT_STATUSES,
1902
+ required: true
1903
+ },
1904
+ occurredAt: {
1905
+ type: Date,
1906
+ required: true
1907
+ },
1908
+ by: { type: String }
1909
+ }, { _id: false });
1910
+ function buildEnrollmentSchema(config) {
1911
+ const schema = new Schema({
1912
+ organizationId: tenantFieldDef(config),
1913
+ sequenceId: {
1914
+ type: config.tenantFieldType === "objectId" ? Schema.Types.ObjectId : String,
1915
+ required: true,
1916
+ ref: "Sequence"
1917
+ },
1918
+ subjectKind: {
1919
+ type: String,
1920
+ enum: ENROLLMENT_SUBJECT_KINDS,
1921
+ required: true
1922
+ },
1923
+ subjectId: {
1924
+ type: config.tenantFieldType === "objectId" ? Schema.Types.ObjectId : String,
1925
+ required: true
1926
+ },
1927
+ status: {
1928
+ type: String,
1929
+ enum: ENROLLMENT_STATUSES,
1930
+ default: "active",
1931
+ required: true
1932
+ },
1933
+ currentStepIndex: {
1934
+ type: Number,
1935
+ default: 0,
1936
+ min: 0
1937
+ },
1938
+ stepResults: {
1939
+ type: [stepResultSchema],
1940
+ default: []
1941
+ },
1942
+ resumeAt: { type: Date },
1943
+ workflowRunId: { type: String },
1944
+ exitReason: {
1945
+ type: String,
1946
+ enum: [
1947
+ "completed",
1948
+ "reply",
1949
+ "unsubscribe",
1950
+ "bounce",
1951
+ "manual"
1952
+ ]
1953
+ },
1954
+ enrolledAt: {
1955
+ type: Date,
1956
+ required: true,
1957
+ default: () => /* @__PURE__ */ new Date()
1958
+ },
1959
+ completedAt: { type: Date },
1960
+ ownerId: { type: String },
1961
+ metadata: { type: Schema.Types.Mixed },
1962
+ statusHistory: {
1963
+ type: [statusEntrySchema],
1964
+ default: []
1965
+ }
1966
+ }, { timestamps: true });
1967
+ schema.index({
1968
+ organizationId: 1,
1969
+ sequenceId: 1,
1970
+ subjectId: 1,
1971
+ status: 1
1972
+ }, { unique: false });
1973
+ schema.index({
1974
+ organizationId: 1,
1975
+ sequenceId: 1,
1976
+ status: 1
1977
+ });
1978
+ schema.index({
1979
+ organizationId: 1,
1980
+ subjectKind: 1,
1981
+ subjectId: 1
1982
+ });
1983
+ schema.index({
1984
+ organizationId: 1,
1985
+ createdAt: -1
1986
+ });
1987
+ return schema;
1988
+ }
1989
+ //#endregion
1611
1990
  //#region src/models/create-models.ts
1612
1991
  /** Mongoose model names this package registers — drives the §21 collision check. */
1613
1992
  const CRM_MODEL_NAMES = [
@@ -1618,12 +1997,15 @@ const CRM_MODEL_NAMES = [
1618
1997
  "Account",
1619
1998
  "Campaign",
1620
1999
  "EmailSubscription",
2000
+ "Suppression",
1621
2001
  "Activity",
1622
2002
  "Note",
1623
2003
  "LossReason",
1624
2004
  "EmailEvent",
1625
2005
  "Conversation",
1626
- "Message"
2006
+ "Message",
2007
+ "Sequence",
2008
+ "Enrollment"
1627
2009
  ];
1628
2010
  function createCrmModels(connection, config) {
1629
2011
  const forceRecreate = config.forceRecreate ?? false;
@@ -1644,12 +2026,15 @@ function createCrmModels(connection, config) {
1644
2026
  Account: register("Account", buildAccountSchema(config)),
1645
2027
  Campaign: register("Campaign", buildCampaignSchema(config)),
1646
2028
  Subscription: register("EmailSubscription", buildSubscriptionSchema(config)),
2029
+ Suppression: register("Suppression", buildSuppressionSchema(config)),
1647
2030
  Activity: register("Activity", buildActivitySchema(config)),
1648
2031
  Note: register("Note", buildNoteSchema(config)),
1649
2032
  LossReason: register("LossReason", buildLossReasonSchema(config)),
1650
2033
  EmailEvent: register("EmailEvent", buildEmailEventSchema(config)),
1651
2034
  Conversation: register("Conversation", buildConversationSchema(config)),
1652
- Message: register("Message", buildMessageSchema(config))
2035
+ Message: register("Message", buildMessageSchema(config)),
2036
+ Sequence: register("Sequence", buildSequenceSchema(config)),
2037
+ Enrollment: register("Enrollment", buildEnrollmentSchema(config))
1653
2038
  });
1654
2039
  }
1655
2040
  //#endregion
@@ -1688,6 +2073,16 @@ const CRM_EVENTS = {
1688
2073
  SUBSCRIPTION_UNSUBSCRIBED: "crm:subscription.unsubscribed",
1689
2074
  SUBSCRIPTION_BOUNCED: "crm:subscription.bounced",
1690
2075
  SUBSCRIPTION_COMPLAINED: "crm:subscription.complained",
2076
+ SEQUENCE_CREATED: "crm:sequence.created",
2077
+ SEQUENCE_ACTIVATED: "crm:sequence.active",
2078
+ SEQUENCE_PAUSED: "crm:sequence.paused",
2079
+ SEQUENCE_ARCHIVED: "crm:sequence.archived",
2080
+ ENROLLMENT_CREATED: "crm:enrollment.created",
2081
+ ENROLLMENT_STEP_SENT: "crm:enrollment.step_sent",
2082
+ ENROLLMENT_PAUSED: "crm:enrollment.paused",
2083
+ ENROLLMENT_RESUMED: "crm:enrollment.resumed",
2084
+ ENROLLMENT_COMPLETED: "crm:enrollment.completed",
2085
+ ENROLLMENT_EXITED: "crm:enrollment.exited",
1691
2086
  EMAIL_EVENT_OPENED: "crm:email_event.opened",
1692
2087
  EMAIL_EVENT_CLICKED: "crm:email_event.clicked",
1693
2088
  EMAIL_EVENT_BOUNCED: "crm:email_event.bounced",
@@ -1706,6 +2101,8 @@ const CRM_EVENTS = {
1706
2101
  OPPORTUNITY_EXTENDED: "crm:opportunity.extended",
1707
2102
  ACTIVITY_RESCHEDULED: "crm:activity.rescheduled",
1708
2103
  ACTIVITY_OUTCOME_RECORDED: "crm:activity.outcome_recorded",
2104
+ SUPPRESSION_ADDED: "crm:suppression.added",
2105
+ SUPPRESSION_REMOVED: "crm:suppression.removed",
1709
2106
  CONVERSATION_CREATED: "crm:conversation.created",
1710
2107
  CONVERSATION_ASSIGNED: "crm:conversation.assigned",
1711
2108
  CONVERSATION_RESOLVED: "crm:conversation.resolved",
@@ -1778,7 +2175,7 @@ var PipelineRepository = class extends Repository {
1778
2175
  async archive(id, ctx) {
1779
2176
  const updated = await this.update(id, { isArchived: true }, ctx);
1780
2177
  if (!updated) throw new NotFoundError("Pipeline", id);
1781
- await this.events.publish(createEvent(CRM_EVENTS.PIPELINE_ARCHIVED, { pipelineId: id }, ctx));
2178
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.PIPELINE_ARCHIVED, { pipelineId: id }, ctx));
1782
2179
  return updated;
1783
2180
  }
1784
2181
  };
@@ -1844,17 +2241,38 @@ const ACTIVITY_MACHINE = defineStateMachine({
1844
2241
  activityId: entityId
1845
2242
  })
1846
2243
  });
2244
+ const SEQUENCE_MACHINE = defineStateMachine({
2245
+ name: "Sequence",
2246
+ transitions: SEQUENCE_TRANSITIONS,
2247
+ errorFactory: ({ entityId, from, to }) => new InvalidSequenceTransitionError({
2248
+ from,
2249
+ to,
2250
+ sequenceId: entityId
2251
+ })
2252
+ });
2253
+ const ENROLLMENT_MACHINE = defineStateMachine({
2254
+ name: "Enrollment",
2255
+ transitions: ENROLLMENT_TRANSITIONS,
2256
+ errorFactory: ({ entityId, from, to }) => new InvalidEnrollmentTransitionError({
2257
+ from,
2258
+ to,
2259
+ enrollmentId: entityId
2260
+ })
2261
+ });
1847
2262
  //#endregion
1848
2263
  //#region src/shared/calculators/scoring.ts
1849
2264
  /**
1850
2265
  * Reference implementation. Intentionally simple — hosts read it and
1851
- * replace it.
2266
+ * replace it. Deterministic + pure (no I/O, no wall-clock).
1852
2267
  *
1853
- * +20 if email is present
1854
- * +20 if phone is present
1855
- * +20 if companyName is present
1856
- * +10 per populated tag up to 40
1857
- * +10 if jobTitle hints senior (director / manager / head / chief / vp / founder)
2268
+ * Firmographic fit (max 110):
2269
+ * +20 email · +20 phone · +20 companyName
2270
+ * +10 per populated tag, up to +40
2271
+ * +10 if jobTitle hints senior (director/manager/head/chief/vp/founder)
2272
+ *
2273
+ * Behavioural engagement, capped at +80 so a run of opens can't dominate a
2274
+ * genuine reply: reply ×25 · click ×8 · open ×2 · activity ×5. Counters are
2275
+ * fed by the host via `LeadRepository.recordEngagement`; absent on old rows.
1858
2276
  */
1859
2277
  function defaultLeadScorer(lead) {
1860
2278
  let score = 0;
@@ -1863,6 +2281,11 @@ function defaultLeadScorer(lead) {
1863
2281
  if (lead.companyName) score += 20;
1864
2282
  score += Math.min(40, (lead.tags?.length ?? 0) * 10);
1865
2283
  if (lead.jobTitle && /(director|manager|head|chief|vp|founder)/i.test(lead.jobTitle)) score += 10;
2284
+ const e = lead.engagement;
2285
+ if (e) {
2286
+ const engagementPoints = (e.replies ?? 0) * 25 + (e.clicks ?? 0) * 8 + (e.opens ?? 0) * 2 + (e.activities ?? 0) * 5;
2287
+ score += Math.min(80, engagementPoints);
2288
+ }
1866
2289
  return score;
1867
2290
  }
1868
2291
  /** Compose multiple small rules into one scorer. */
@@ -1871,6 +2294,13 @@ function composeScorers(...rules) {
1871
2294
  }
1872
2295
  //#endregion
1873
2296
  //#region src/repositories/lead.repository.ts
2297
+ /** signal → engagement counter field. */
2298
+ const ENGAGEMENT_COUNTER = {
2299
+ open: "opens",
2300
+ click: "clicks",
2301
+ reply: "replies",
2302
+ activity: "activities"
2303
+ };
1874
2304
  var LeadRepository = class extends Repository {
1875
2305
  events;
1876
2306
  scorer;
@@ -1913,12 +2343,12 @@ var LeadRepository = class extends Repository {
1913
2343
  let result = created;
1914
2344
  if (score !== 0) {
1915
2345
  result = await this.update(String(created._id), { score }, ctx) ?? created;
1916
- await this.events.publish(createEvent(CRM_EVENTS.LEAD_SCORED, {
2346
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.LEAD_SCORED, {
1917
2347
  leadId: String(created._id),
1918
2348
  score
1919
2349
  }, ctx));
1920
2350
  }
1921
- await this.events.publish(createEvent(CRM_EVENTS.LEAD_CREATED, {
2351
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.LEAD_CREATED, {
1922
2352
  leadId: String(result._id),
1923
2353
  email: result.email,
1924
2354
  score: result.score
@@ -1937,7 +2367,7 @@ var LeadRepository = class extends Repository {
1937
2367
  note: reason,
1938
2368
  patch: { disqualifyReason: reason }
1939
2369
  }, ctx);
1940
- await this.events.publish(createEvent(CRM_EVENTS.LEAD_DISQUALIFIED, {
2370
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.LEAD_DISQUALIFIED, {
1941
2371
  leadId: id,
1942
2372
  reason
1943
2373
  }, ctx));
@@ -1959,7 +2389,7 @@ var LeadRepository = class extends Repository {
1959
2389
  if (!await this.getById(id, ctx)) throw new NotFoundError("Lead", id);
1960
2390
  if (!await this.update(id, patch, ctx)) throw new NotFoundError("Lead", id);
1961
2391
  const fields = Object.keys(patch);
1962
- await this.events.publish(createEvent(CRM_EVENTS.LEAD_ENRICHED, {
2392
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.LEAD_ENRICHED, {
1963
2393
  leadId: id,
1964
2394
  fields,
1965
2395
  ...extras.provider !== void 0 ? { provider: extras.provider } : {}
@@ -1983,7 +2413,7 @@ var LeadRepository = class extends Repository {
1983
2413
  if (!current) throw new NotFoundError("Lead", id);
1984
2414
  return current;
1985
2415
  }
1986
- await this.events.publish(createEvent(CRM_EVENTS.LEAD_ASSIGNED, {
2416
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.LEAD_ASSIGNED, {
1987
2417
  leadId: id,
1988
2418
  ownerId,
1989
2419
  ...before.ownerId !== void 0 ? { previousOwnerId: before.ownerId } : {}
@@ -1991,6 +2421,20 @@ var LeadRepository = class extends Repository {
1991
2421
  return await this.getById(id, ctx) ?? before;
1992
2422
  }
1993
2423
  /**
2424
+ * Record a behavioural engagement signal, then re-score. The host feeds
2425
+ * these off CRM events (`crm:message.received` → reply, `crm:activity.*` →
2426
+ * activity, and email open/click tracking → open/click). Atomic `$inc`
2427
+ * on the counter (single round-trip, no lost-update window under
2428
+ * concurrent signals), then `recomputeScore` re-reads the fresh counters.
2429
+ */
2430
+ async recordEngagement(id, signal, ctx) {
2431
+ if (!await this.findOneAndUpdate({ _id: id }, {
2432
+ $inc: { [`engagement.${ENGAGEMENT_COUNTER[signal]}`]: 1 },
2433
+ $set: { "engagement.lastEngagedAt": /* @__PURE__ */ new Date() }
2434
+ }, ctx)) throw new NotFoundError("Lead", id);
2435
+ return this.recomputeScore(id, ctx);
2436
+ }
2437
+ /**
1994
2438
  * Recompute + persist the lead's score. Called from `createLead`
1995
2439
  * automatically; hosts also call this after enrichment writes.
1996
2440
  */
@@ -2001,7 +2445,7 @@ var LeadRepository = class extends Repository {
2001
2445
  if (score === lead.score) return lead;
2002
2446
  const updated = await this.update(id, { score }, ctx);
2003
2447
  if (!updated) throw new NotFoundError("Lead", id);
2004
- await this.events.publish(createEvent(CRM_EVENTS.LEAD_SCORED, {
2448
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.LEAD_SCORED, {
2005
2449
  leadId: id,
2006
2450
  score
2007
2451
  }, ctx));
@@ -2031,7 +2475,7 @@ var LeadRepository = class extends Repository {
2031
2475
  to: target,
2032
2476
  leadId: id
2033
2477
  });
2034
- await this.events.publish(createEvent(CRM_EVENTS.LEAD_STATUS_CHANGED, {
2478
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.LEAD_STATUS_CHANGED, {
2035
2479
  leadId: id,
2036
2480
  from: existing.status,
2037
2481
  to: target
@@ -2097,7 +2541,7 @@ var OpportunityRepository = class extends Repository {
2097
2541
  ...input.metadata !== void 0 ? { metadata: input.metadata } : {}
2098
2542
  };
2099
2543
  const created = await this.create(draft, ctx);
2100
- await this.events.publish(createEvent(CRM_EVENTS.OPPORTUNITY_CREATED, {
2544
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.OPPORTUNITY_CREATED, {
2101
2545
  opportunityId: String(created._id),
2102
2546
  pipelineId: input.pipelineId,
2103
2547
  stageId: String(stage._id)
@@ -2141,7 +2585,7 @@ var OpportunityRepository = class extends Repository {
2141
2585
  stageId: String(target._id)
2142
2586
  });
2143
2587
  const event = target.sequence > current.sequence ? CRM_EVENTS.OPPORTUNITY_STAGE_ADVANCED : CRM_EVENTS.OPPORTUNITY_STAGE_REGRESSED;
2144
- await this.events.publish(createEvent(event, {
2588
+ await this.events.dispatch(ctx, createEvent(event, {
2145
2589
  opportunityId: id,
2146
2590
  from: {
2147
2591
  stageId: String(current._id),
@@ -2163,7 +2607,7 @@ var OpportunityRepository = class extends Repository {
2163
2607
  ...input.by !== void 0 ? { by: input.by } : {},
2164
2608
  ...input.note !== void 0 ? { note: input.note } : {}
2165
2609
  }, ctx);
2166
- await this.events.publish(createEvent(CRM_EVENTS.OPPORTUNITY_WON, {
2610
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.OPPORTUNITY_WON, {
2167
2611
  opportunityId: id,
2168
2612
  amount: updated.amount
2169
2613
  }, ctx));
@@ -2178,7 +2622,7 @@ var OpportunityRepository = class extends Repository {
2178
2622
  ...input.by !== void 0 ? { by: input.by } : {},
2179
2623
  ...input.note !== void 0 ? { note: input.note } : {}
2180
2624
  }, ctx);
2181
- await this.events.publish(createEvent(CRM_EVENTS.OPPORTUNITY_LOST, {
2625
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.OPPORTUNITY_LOST, {
2182
2626
  opportunityId: id,
2183
2627
  lostReasonId: updated.lostReasonId
2184
2628
  }, ctx));
@@ -2201,7 +2645,7 @@ var OpportunityRepository = class extends Repository {
2201
2645
  if (!current) throw new NotFoundError("Opportunity", id);
2202
2646
  return current;
2203
2647
  }
2204
- await this.events.publish(createEvent(CRM_EVENTS.OPPORTUNITY_ASSIGNED, {
2648
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.OPPORTUNITY_ASSIGNED, {
2205
2649
  opportunityId: id,
2206
2650
  ownerId,
2207
2651
  ...before.ownerId !== void 0 ? { previousOwnerId: before.ownerId } : {}
@@ -2225,7 +2669,7 @@ var OpportunityRepository = class extends Repository {
2225
2669
  if (amount === existing.amount) return existing;
2226
2670
  const updated = await this.update(id, { amount }, ctx);
2227
2671
  if (!updated) throw new NotFoundError("Opportunity", id);
2228
- await this.events.publish(createEvent(CRM_EVENTS.OPPORTUNITY_AMOUNT_CHANGED, {
2672
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.OPPORTUNITY_AMOUNT_CHANGED, {
2229
2673
  opportunityId: id,
2230
2674
  amount,
2231
2675
  ...existing.amount !== void 0 ? { previousAmount: existing.amount } : {},
@@ -2252,7 +2696,7 @@ var OpportunityRepository = class extends Repository {
2252
2696
  if (previous && newDate <= previous) return existing;
2253
2697
  const updated = await this.update(id, { expectedCloseAt: newDate }, ctx);
2254
2698
  if (!updated) throw new NotFoundError("Opportunity", id);
2255
- await this.events.publish(createEvent(CRM_EVENTS.OPPORTUNITY_EXTENDED, {
2699
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.OPPORTUNITY_EXTENDED, {
2256
2700
  opportunityId: id,
2257
2701
  expectedCloseAt: newDate.toISOString(),
2258
2702
  ...previous ? { previousExpectedCloseAt: previous.toISOString() } : {},
@@ -2269,7 +2713,7 @@ var OpportunityRepository = class extends Repository {
2269
2713
  ...by !== void 0 ? { by } : {},
2270
2714
  ...note !== void 0 ? { note } : {}
2271
2715
  }, ctx);
2272
- await this.events.publish(createEvent(CRM_EVENTS.OPPORTUNITY_ABANDONED, { opportunityId: id }, ctx));
2716
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.OPPORTUNITY_ABANDONED, { opportunityId: id }, ctx));
2273
2717
  return updated;
2274
2718
  }
2275
2719
  async _closeStatus(id, target, patch, extras, ctx) {
@@ -2366,7 +2810,7 @@ var ContactRepository = class extends Repository {
2366
2810
  */
2367
2811
  async createContact(input, ctx) {
2368
2812
  const created = await this.create(input, ctx);
2369
- await this.events.publish(createEvent(CRM_EVENTS.CONTACT_CREATED, {
2813
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.CONTACT_CREATED, {
2370
2814
  contactId: String(created._id),
2371
2815
  email: created.contact?.email
2372
2816
  }, ctx));
@@ -2381,7 +2825,7 @@ var ContactRepository = class extends Repository {
2381
2825
  if (!await this.getById(id, ctx)) throw new NotFoundError("Contact", id);
2382
2826
  const updated = await this.update(id, patch, ctx);
2383
2827
  if (!updated) throw new NotFoundError("Contact", id);
2384
- await this.events.publish(createEvent(CRM_EVENTS.CONTACT_ENRICHED, {
2828
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.CONTACT_ENRICHED, {
2385
2829
  contactId: id,
2386
2830
  fields: Object.keys(patch),
2387
2831
  ...extras.provider !== void 0 ? { provider: extras.provider } : {}
@@ -2402,7 +2846,7 @@ var ContactRepository = class extends Repository {
2402
2846
  if (!current) throw new NotFoundError("Contact", id);
2403
2847
  return current;
2404
2848
  }
2405
- await this.events.publish(createEvent(CRM_EVENTS.CONTACT_ASSIGNED, {
2849
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.CONTACT_ASSIGNED, {
2406
2850
  contactId: id,
2407
2851
  ownerId,
2408
2852
  ...before.ownerId !== void 0 ? { previousOwnerId: before.ownerId } : {}
@@ -2452,7 +2896,7 @@ var ContactRepository = class extends Repository {
2452
2896
  const updated = await this.update(primaryId, patch, ctx);
2453
2897
  if (!updated) throw new NotFoundError("Contact", primaryId);
2454
2898
  await this.delete(secondaryId, ctx);
2455
- await this.events.publish(createEvent(CRM_EVENTS.CONTACT_MERGED, {
2899
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.CONTACT_MERGED, {
2456
2900
  primaryId,
2457
2901
  secondaryId,
2458
2902
  strategy
@@ -2484,7 +2928,7 @@ var AccountRepository = class extends Repository {
2484
2928
  }
2485
2929
  async createAccount(input, ctx) {
2486
2930
  const created = await this.create(input, ctx);
2487
- await this.events.publish(createEvent(CRM_EVENTS.ACCOUNT_CREATED, {
2931
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ACCOUNT_CREATED, {
2488
2932
  accountId: String(created._id),
2489
2933
  name: created.name
2490
2934
  }, ctx));
@@ -2494,7 +2938,7 @@ var AccountRepository = class extends Repository {
2494
2938
  if (!await this.getById(id, ctx)) throw new NotFoundError("Account", id);
2495
2939
  const updated = await this.update(id, patch, ctx);
2496
2940
  if (!updated) throw new NotFoundError("Account", id);
2497
- await this.events.publish(createEvent(CRM_EVENTS.ACCOUNT_ENRICHED, {
2941
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ACCOUNT_ENRICHED, {
2498
2942
  accountId: id,
2499
2943
  fields: Object.keys(patch),
2500
2944
  ...extras.provider !== void 0 ? { provider: extras.provider } : {}
@@ -2514,7 +2958,7 @@ var AccountRepository = class extends Repository {
2514
2958
  if (!current) throw new NotFoundError("Account", id);
2515
2959
  return current;
2516
2960
  }
2517
- await this.events.publish(createEvent(CRM_EVENTS.ACCOUNT_ASSIGNED, {
2961
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ACCOUNT_ASSIGNED, {
2518
2962
  accountId: id,
2519
2963
  ownerId,
2520
2964
  ...before.ownerId !== void 0 ? { previousOwnerId: before.ownerId } : {}
@@ -2543,7 +2987,7 @@ var AccountRepository = class extends Repository {
2543
2987
  const updated = await this.update(primaryId, patch, ctx);
2544
2988
  if (!updated) throw new NotFoundError("Account", primaryId);
2545
2989
  await this.delete(secondaryId, ctx);
2546
- await this.events.publish(createEvent(CRM_EVENTS.ACCOUNT_MERGED, {
2990
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ACCOUNT_MERGED, {
2547
2991
  primaryId,
2548
2992
  secondaryId,
2549
2993
  strategy
@@ -2587,7 +3031,7 @@ var CampaignRepository = class extends Repository {
2587
3031
  if (this.rejectDuplicates && input.name && isDuplicateKeyError(err)) throw new DuplicateCampaignError(input.name);
2588
3032
  throw err;
2589
3033
  }
2590
- await this.events.publish(createEvent(CRM_EVENTS.CAMPAIGN_CREATED, {
3034
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.CAMPAIGN_CREATED, {
2591
3035
  campaignId: String(created._id),
2592
3036
  name: created.name
2593
3037
  }, ctx));
@@ -2595,7 +3039,7 @@ var CampaignRepository = class extends Repository {
2595
3039
  }
2596
3040
  async activate(id, by, ctx) {
2597
3041
  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));
3042
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.CAMPAIGN_ACTIVATED, { campaignId: id }, ctx));
2599
3043
  return updated;
2600
3044
  }
2601
3045
  async pause(id, reason, by, ctx) {
@@ -2603,7 +3047,7 @@ var CampaignRepository = class extends Repository {
2603
3047
  ...by !== void 0 ? { by } : {},
2604
3048
  ...reason !== void 0 ? { note: reason } : {}
2605
3049
  }, ctx);
2606
- await this.events.publish(createEvent(CRM_EVENTS.CAMPAIGN_PAUSED, {
3050
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.CAMPAIGN_PAUSED, {
2607
3051
  campaignId: id,
2608
3052
  reason
2609
3053
  }, ctx));
@@ -2611,13 +3055,13 @@ var CampaignRepository = class extends Repository {
2611
3055
  }
2612
3056
  async resume(id, by, ctx) {
2613
3057
  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));
3058
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.CAMPAIGN_RESUMED, { campaignId: id }, ctx));
2615
3059
  return updated;
2616
3060
  }
2617
3061
  async complete(id, extras = {}, ctx) {
2618
3062
  const patch = extras.actualRevenueAmount !== void 0 ? { actualRevenueAmount: extras.actualRevenueAmount } : {};
2619
3063
  const updated = await this._transition(id, "completed", extras, ctx, patch);
2620
- await this.events.publish(createEvent(CRM_EVENTS.CAMPAIGN_COMPLETED, {
3064
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.CAMPAIGN_COMPLETED, {
2621
3065
  campaignId: id,
2622
3066
  actualRevenueAmount: updated.actualRevenueAmount
2623
3067
  }, ctx));
@@ -2625,7 +3069,7 @@ var CampaignRepository = class extends Repository {
2625
3069
  }
2626
3070
  async archive(id, by, ctx) {
2627
3071
  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));
3072
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.CAMPAIGN_ARCHIVED, { campaignId: id }, ctx));
2629
3073
  return updated;
2630
3074
  }
2631
3075
  async _transition(id, target, extras, ctx, patch = {}) {
@@ -2716,7 +3160,7 @@ var SubscriptionRepository = class extends Repository {
2716
3160
  });
2717
3161
  throw err;
2718
3162
  }
2719
- await this.events.publish(createEvent(CRM_EVENTS.SUBSCRIPTION_CREATED, {
3163
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.SUBSCRIPTION_CREATED, {
2720
3164
  subscriptionId: String(sub._id),
2721
3165
  contactId: sub.contactId,
2722
3166
  campaignId: sub.campaignId,
@@ -2729,7 +3173,7 @@ var SubscriptionRepository = class extends Repository {
2729
3173
  if (!sub || sub.status !== "pending") throw new InvalidConfirmationTokenError();
2730
3174
  const confirmedAt = /* @__PURE__ */ new Date();
2731
3175
  const updated = await this._transition(String(sub._id), "confirmed", { patch: { confirmedAt } }, ctx);
2732
- await this.events.publish(createEvent(CRM_EVENTS.SUBSCRIPTION_CONFIRMED, {
3176
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.SUBSCRIPTION_CONFIRMED, {
2733
3177
  subscriptionId: String(sub._id),
2734
3178
  contactId: sub.contactId,
2735
3179
  campaignId: sub.campaignId,
@@ -2749,7 +3193,7 @@ var SubscriptionRepository = class extends Repository {
2749
3193
  ...reason !== void 0 ? { unsubscribeReason: reason } : {}
2750
3194
  }
2751
3195
  }, ctx);
2752
- await this.events.publish(createEvent(CRM_EVENTS.SUBSCRIPTION_UNSUBSCRIBED, {
3196
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.SUBSCRIPTION_UNSUBSCRIBED, {
2753
3197
  subscriptionId: String(sub._id),
2754
3198
  contactId: sub.contactId,
2755
3199
  campaignId: sub.campaignId,
@@ -2769,7 +3213,7 @@ var SubscriptionRepository = class extends Repository {
2769
3213
  ...reason !== void 0 ? { bounceReason: reason } : {}
2770
3214
  }
2771
3215
  }, ctx);
2772
- await this.events.publish(createEvent(CRM_EVENTS.SUBSCRIPTION_BOUNCED, {
3216
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.SUBSCRIPTION_BOUNCED, {
2773
3217
  subscriptionId: id,
2774
3218
  contactId: existing.contactId,
2775
3219
  campaignId: existing.campaignId,
@@ -2783,7 +3227,7 @@ var SubscriptionRepository = class extends Repository {
2783
3227
  if (existing.status === "complained") return existing;
2784
3228
  const complainedAt = /* @__PURE__ */ new Date();
2785
3229
  const updated = await this._transition(id, "complained", { patch: { complainedAt } }, ctx);
2786
- await this.events.publish(createEvent(CRM_EVENTS.SUBSCRIPTION_COMPLAINED, {
3230
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.SUBSCRIPTION_COMPLAINED, {
2787
3231
  subscriptionId: id,
2788
3232
  contactId: existing.contactId,
2789
3233
  campaignId: existing.campaignId
@@ -2818,6 +3262,78 @@ var SubscriptionRepository = class extends Repository {
2818
3262
  }
2819
3263
  };
2820
3264
  //#endregion
3265
+ //#region src/repositories/suppression.repository.ts
3266
+ function normalizeEmail(email) {
3267
+ return email.trim().toLowerCase();
3268
+ }
3269
+ var SuppressionRepository = class extends Repository {
3270
+ events;
3271
+ constructor(model, plugins, deps, pagination) {
3272
+ super(model, plugins, pagination);
3273
+ this.events = deps.events;
3274
+ }
3275
+ /**
3276
+ * Idempotently add `email` to the org's suppression list. Atomic
3277
+ * `getOrCreate` on the unique (org, email) index — of N concurrent
3278
+ * calls exactly one inserts. Emits `crm:suppression.added` only for
3279
+ * the inserting call; an existing row is returned unchanged, no event.
3280
+ */
3281
+ async suppress(email, input, ctx) {
3282
+ const normalized = normalizeEmail(email);
3283
+ const draft = {
3284
+ email: normalized,
3285
+ reason: input.reason,
3286
+ suppressedAt: /* @__PURE__ */ new Date(),
3287
+ ...input.source !== void 0 ? { source: input.source } : {},
3288
+ ...input.subjectKind !== void 0 ? { subjectKind: input.subjectKind } : {},
3289
+ ...input.subjectId !== void 0 ? { subjectId: input.subjectId } : {},
3290
+ ...input.metadata !== void 0 ? { metadata: input.metadata } : {}
3291
+ };
3292
+ const { doc, created } = await this.getOrCreate({ email: normalized }, draft, ctx);
3293
+ if (created) await this.events.dispatch(ctx, createEvent(CRM_EVENTS.SUPPRESSION_ADDED, {
3294
+ email: normalized,
3295
+ reason: input.reason,
3296
+ ...input.source !== void 0 ? { source: input.source } : {}
3297
+ }, ctx));
3298
+ return doc;
3299
+ }
3300
+ /** True when `email` is on the org's suppression list. */
3301
+ async isSuppressed(email, ctx) {
3302
+ return await this.exists({ email: normalizeEmail(email) }, ctx) !== null;
3303
+ }
3304
+ /**
3305
+ * Return the suppressed subset of `emails` (normalized) as a Set —
3306
+ * ONE `$in` query, built for the campaign-fanout hot path:
3307
+ *
3308
+ * ```ts
3309
+ * const blocked = await suppression.filterSuppressed(recipients, ctx);
3310
+ * const sendable = recipients.filter((e) => !blocked.has(e.toLowerCase()));
3311
+ * ```
3312
+ */
3313
+ async filterSuppressed(emails, ctx) {
3314
+ if (emails.length === 0) return /* @__PURE__ */ new Set();
3315
+ const normalized = [...new Set(emails.map(normalizeEmail))];
3316
+ const rows = await this.findAll({ email: { $in: normalized } }, {
3317
+ ...ctx ?? {},
3318
+ select: "email",
3319
+ lean: true
3320
+ });
3321
+ return new Set(rows.map((row) => row.email));
3322
+ }
3323
+ /**
3324
+ * Remove `email` from the org's suppression list. Single atomic
3325
+ * `deleteMany` on the (unique) email key — emits
3326
+ * `crm:suppression.removed` only when a row was actually removed.
3327
+ * Returns whether a row was removed.
3328
+ */
3329
+ async unsuppress(email, ctx) {
3330
+ const normalized = normalizeEmail(email);
3331
+ const removed = (await this.deleteMany({ email: normalized }, ctx ?? {})).deletedCount > 0;
3332
+ if (removed) await this.events.dispatch(ctx, createEvent(CRM_EVENTS.SUPPRESSION_REMOVED, { email: normalized }, ctx));
3333
+ return removed;
3334
+ }
3335
+ };
3336
+ //#endregion
2821
3337
  //#region src/repositories/activity.repository.ts
2822
3338
  var ActivityRepository = class extends Repository {
2823
3339
  events;
@@ -2831,7 +3347,7 @@ var ActivityRepository = class extends Repository {
2831
3347
  status: "planned"
2832
3348
  };
2833
3349
  const created = await this.create(draft, ctx);
2834
- await this.events.publish(createEvent(CRM_EVENTS.ACTIVITY_PLANNED, {
3350
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ACTIVITY_PLANNED, {
2835
3351
  activityId: String(created._id),
2836
3352
  type: created.type,
2837
3353
  subjectKind: created.subjectKind,
@@ -2845,7 +3361,7 @@ var ActivityRepository = class extends Repository {
2845
3361
  async complete(id, completedAt, ctx) {
2846
3362
  const ts = completedAt ?? /* @__PURE__ */ new Date();
2847
3363
  const updated = await this._transition(id, "completed", { completedAt: ts }, ctx);
2848
- await this.events.publish(createEvent(CRM_EVENTS.ACTIVITY_COMPLETED, {
3364
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ACTIVITY_COMPLETED, {
2849
3365
  activityId: id,
2850
3366
  completedAt: ts.toISOString()
2851
3367
  }, ctx));
@@ -2853,7 +3369,7 @@ var ActivityRepository = class extends Repository {
2853
3369
  }
2854
3370
  async cancel(id, reason, ctx) {
2855
3371
  const updated = await this._transition(id, "cancelled", reason ? { metadata: { cancelReason: reason } } : {}, ctx);
2856
- await this.events.publish(createEvent(CRM_EVENTS.ACTIVITY_CANCELLED, {
3372
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ACTIVITY_CANCELLED, {
2857
3373
  activityId: id,
2858
3374
  ...reason !== void 0 ? { reason } : {}
2859
3375
  }, ctx));
@@ -2875,7 +3391,7 @@ var ActivityRepository = class extends Repository {
2875
3391
  const previous = existing.dueDate;
2876
3392
  const updated = await this.update(id, { dueDate: newDueDate }, ctx);
2877
3393
  if (!updated) throw new NotFoundError("Activity", id);
2878
- await this.events.publish(createEvent(CRM_EVENTS.ACTIVITY_RESCHEDULED, {
3394
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ACTIVITY_RESCHEDULED, {
2879
3395
  activityId: id,
2880
3396
  dueDate: newDueDate.toISOString(),
2881
3397
  ...previous ? { previousDueDate: previous.toISOString() } : {},
@@ -2902,7 +3418,7 @@ var ActivityRepository = class extends Repository {
2902
3418
  };
2903
3419
  const updated = await this.update(id, { metadata: mergedMetadata }, ctx);
2904
3420
  if (!updated) throw new NotFoundError("Activity", id);
2905
- await this.events.publish(createEvent(CRM_EVENTS.ACTIVITY_OUTCOME_RECORDED, {
3421
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ACTIVITY_OUTCOME_RECORDED, {
2906
3422
  activityId: id,
2907
3423
  outcome
2908
3424
  }, ctx));
@@ -2959,7 +3475,7 @@ var NoteRepository = class extends Repository {
2959
3475
  }
2960
3476
  async addNote(input, ctx) {
2961
3477
  const created = await this.create(input, ctx);
2962
- await this.events.publish(createEvent(CRM_EVENTS.NOTE_ADDED, {
3478
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.NOTE_ADDED, {
2963
3479
  noteId: String(created._id),
2964
3480
  subjectKind: created.subjectKind,
2965
3481
  subjectId: String(created.subjectId)
@@ -3025,7 +3541,7 @@ var EmailEventRepository = class extends Repository {
3025
3541
  ...input.metadata !== void 0 ? { metadata: input.metadata } : {}
3026
3542
  };
3027
3543
  const created = await this.create(draft, ctx);
3028
- await this.events.publish(createEvent(KIND_TO_EVENT[kind], {
3544
+ await this.events.dispatch(ctx, createEvent(KIND_TO_EVENT[kind], {
3029
3545
  eventId: String(created._id),
3030
3546
  kind,
3031
3547
  subjectKind: input.subjectKind,
@@ -3055,7 +3571,7 @@ var ConversationRepository = class extends Repository {
3055
3571
  ...input.metadata !== void 0 ? { metadata: input.metadata } : {}
3056
3572
  };
3057
3573
  const created = await this.create(draft, ctx);
3058
- await this.events.publish(createEvent(CRM_EVENTS.CONVERSATION_CREATED, {
3574
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.CONVERSATION_CREATED, {
3059
3575
  conversationId: String(created._id),
3060
3576
  channel: created.channel,
3061
3577
  participantKind: created.participantKind,
@@ -3076,7 +3592,7 @@ var ConversationRepository = class extends Repository {
3076
3592
  if (!current) throw new NotFoundError("Conversation", id);
3077
3593
  return current;
3078
3594
  }
3079
- await this.events.publish(createEvent(CRM_EVENTS.CONVERSATION_ASSIGNED, {
3595
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.CONVERSATION_ASSIGNED, {
3080
3596
  conversationId: id,
3081
3597
  assigneeId,
3082
3598
  ...before.assigneeId !== void 0 ? { previousAssigneeId: before.assigneeId } : {}
@@ -3114,7 +3630,7 @@ var ConversationRepository = class extends Repository {
3114
3630
  if (existing.status === next) return existing;
3115
3631
  const updated = await this.update(id, { status: next }, ctx);
3116
3632
  if (!updated) throw new NotFoundError("Conversation", id);
3117
- await this.events.publish(createEvent(eventType, {
3633
+ await this.events.dispatch(ctx, createEvent(eventType, {
3118
3634
  conversationId: id,
3119
3635
  status: next,
3120
3636
  previousStatus: existing.status
@@ -3166,7 +3682,7 @@ var MessageRepository = class extends Repository {
3166
3682
  direction,
3167
3683
  sentAt
3168
3684
  }, ctx);
3169
- await this.events.publish(createEvent(eventType, {
3685
+ await this.events.dispatch(ctx, createEvent(eventType, {
3170
3686
  messageId: String(created._id),
3171
3687
  conversationId: input.conversationId,
3172
3688
  direction,
@@ -3191,6 +3707,230 @@ var MessageRepository = class extends Repository {
3191
3707
  }
3192
3708
  };
3193
3709
  //#endregion
3710
+ //#region src/repositories/sequence.repository.ts
3711
+ var SequenceRepository = class extends Repository {
3712
+ events;
3713
+ constructor(model, plugins, deps, pagination) {
3714
+ super(model, plugins, pagination);
3715
+ this.events = deps.events;
3716
+ }
3717
+ async activate(id, ctx) {
3718
+ const doc = await this._transition(id, "active", ctx);
3719
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.SEQUENCE_ACTIVATED, { sequenceId: id }, ctx));
3720
+ return doc;
3721
+ }
3722
+ async activateFromPaused(id, ctx) {
3723
+ return this.activate(id, ctx);
3724
+ }
3725
+ async pause(id, ctx) {
3726
+ const doc = await this._transition(id, "paused", ctx);
3727
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.SEQUENCE_PAUSED, { sequenceId: id }, ctx));
3728
+ return doc;
3729
+ }
3730
+ async archive(id, ctx) {
3731
+ const doc = await this._transition(id, "archived", ctx);
3732
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.SEQUENCE_ARCHIVED, { sequenceId: id }, ctx));
3733
+ return doc;
3734
+ }
3735
+ async _transition(id, target, ctx) {
3736
+ const existing = await this.getById(id, ctx);
3737
+ if (!existing) throw new NotFoundError("Sequence", id);
3738
+ SEQUENCE_MACHINE.assertTransition(id, existing.status, target);
3739
+ const entry = {
3740
+ status: target,
3741
+ occurredAt: /* @__PURE__ */ new Date(),
3742
+ ...ctx?.actorId !== void 0 ? { by: String(ctx.actorId) } : {}
3743
+ };
3744
+ const updated = await assertAndClaim(SEQUENCE_MACHINE, this, id, {
3745
+ from: existing.status,
3746
+ to: target,
3747
+ patch: { statusHistory: [...existing.statusHistory ?? [], entry] },
3748
+ options: ctx
3749
+ });
3750
+ if (!updated) throw new InvalidSequenceTransitionError({
3751
+ from: (await this.getById(id, ctx))?.status ?? existing.status,
3752
+ to: target,
3753
+ sequenceId: id
3754
+ });
3755
+ return updated;
3756
+ }
3757
+ };
3758
+ //#endregion
3759
+ //#region src/repositories/enrollment.repository.ts
3760
+ var EnrollmentRepository = class extends Repository {
3761
+ events;
3762
+ sequences;
3763
+ constructor(model, plugins, deps, pagination) {
3764
+ super(model, plugins, pagination);
3765
+ this.events = deps.events;
3766
+ this.sequences = deps.sequences;
3767
+ }
3768
+ /** Create a new enrollment — throws AlreadyEnrolledError if an active one exists. */
3769
+ async enroll(input, ctx) {
3770
+ const { sequenceId, subjectKind, subjectId, workflowRunId, ownerId, metadata } = input;
3771
+ if (await this.getOne({
3772
+ sequenceId,
3773
+ subjectId,
3774
+ status: { $in: ["active", "paused"] }
3775
+ }, ctx)) throw new AlreadyEnrolledError({
3776
+ sequenceId,
3777
+ subjectId
3778
+ });
3779
+ const doc = await this.create({
3780
+ organizationId: ctx?.organizationId,
3781
+ sequenceId,
3782
+ subjectKind,
3783
+ subjectId,
3784
+ status: "active",
3785
+ currentStepIndex: 0,
3786
+ stepResults: [],
3787
+ enrolledAt: /* @__PURE__ */ new Date(),
3788
+ ...workflowRunId ? { workflowRunId } : {},
3789
+ ...ownerId ? { ownerId } : {},
3790
+ ...metadata ? { metadata } : {},
3791
+ statusHistory: [{
3792
+ status: "active",
3793
+ occurredAt: /* @__PURE__ */ new Date()
3794
+ }]
3795
+ }, ctx);
3796
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ENROLLMENT_CREATED, {
3797
+ enrollmentId: String(doc._id),
3798
+ sequenceId,
3799
+ subjectId,
3800
+ subjectKind
3801
+ }, ctx));
3802
+ return doc;
3803
+ }
3804
+ /** Set workflowRunId after the workflow has started. */
3805
+ async setWorkflowRunId(id, workflowRunId, ctx) {
3806
+ await this.update(id, { workflowRunId }, ctx);
3807
+ }
3808
+ /** Atomically advance to the next step and record the result (CAS). */
3809
+ async advanceStep(id, nextStepIndex, stepResult, ctx) {
3810
+ return this.claim(id, {
3811
+ field: "currentStepIndex",
3812
+ from: nextStepIndex - 1,
3813
+ to: nextStepIndex,
3814
+ where: { status: "active" }
3815
+ }, stepResult ? { $push: { stepResults: stepResult } } : {}, ctx);
3816
+ }
3817
+ async pause(id, ctx) {
3818
+ const doc = await this._transition(id, "paused", ctx);
3819
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ENROLLMENT_PAUSED, { enrollmentId: id }, ctx));
3820
+ return doc;
3821
+ }
3822
+ async resume(id, ctx) {
3823
+ const doc = await this._transition(id, "active", ctx);
3824
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ENROLLMENT_RESUMED, { enrollmentId: id }, ctx));
3825
+ return doc;
3826
+ }
3827
+ async complete(id, ctx) {
3828
+ const doc = await this._transition(id, "completed", ctx, { completedAt: /* @__PURE__ */ new Date() });
3829
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ENROLLMENT_COMPLETED, { enrollmentId: id }, ctx));
3830
+ return doc;
3831
+ }
3832
+ async exit(id, exitReason, ctx) {
3833
+ const existing = await this.getById(id, ctx);
3834
+ if (!existing) throw new NotFoundError("Enrollment", id);
3835
+ const from = existing.status;
3836
+ ENROLLMENT_MACHINE.assertTransition(id, from, "exited");
3837
+ const entry = {
3838
+ status: "exited",
3839
+ occurredAt: /* @__PURE__ */ new Date(),
3840
+ ...ctx?.actorId ? { by: String(ctx.actorId) } : {}
3841
+ };
3842
+ const updated = await assertAndClaim(ENROLLMENT_MACHINE, this, id, {
3843
+ from,
3844
+ to: "exited",
3845
+ patch: {
3846
+ exitReason,
3847
+ statusHistory: [...existing.statusHistory ?? [], entry]
3848
+ },
3849
+ options: ctx
3850
+ });
3851
+ if (!updated) throw new NotFoundError("Enrollment", id);
3852
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ENROLLMENT_EXITED, {
3853
+ enrollmentId: id,
3854
+ exitReason
3855
+ }, ctx));
3856
+ return updated;
3857
+ }
3858
+ /**
3859
+ * Reply detection: exit every ACTIVE enrollment of the given subjects
3860
+ * with reason `'reply'` — the one method a host's
3861
+ * `crm:message.received` subscriber calls:
3862
+ *
3863
+ * ```ts
3864
+ * crm.events.subscribe('crm:message.received', async (e) => {
3865
+ * await crm.repositories.enrollment.exitActiveOnReply(
3866
+ * [{ subjectKind: 'contact', subjectId: e.payload.participantId }],
3867
+ * { organizationId: e.meta.organizationId },
3868
+ * );
3869
+ * });
3870
+ * ```
3871
+ *
3872
+ * Sequences are batch-loaded in ONE `$in` query; enrollments whose
3873
+ * sequence has `replyDetectionEnabled === false` are skipped (a missing
3874
+ * sequence — e.g. deleted — does NOT skip: the default is to exit).
3875
+ * Each exit goes through `exit()` (state-machine CAS + statusHistory +
3876
+ * ENROLLMENT_EXITED event). An enrollment that a concurrent transition
3877
+ * already moved out of `active` loses the CAS and is skipped — the
3878
+ * desired end state ("not active") already holds.
3879
+ *
3880
+ * Returns the enrollments this call actually exited.
3881
+ */
3882
+ async exitActiveOnReply(subjects, ctx) {
3883
+ if (subjects.length === 0) return [];
3884
+ const active = await this.findAll({
3885
+ status: "active",
3886
+ $or: subjects.map((s) => ({
3887
+ subjectKind: s.subjectKind,
3888
+ subjectId: s.subjectId
3889
+ }))
3890
+ }, ctx);
3891
+ if (active.length === 0) return [];
3892
+ const sequenceIds = [...new Set(active.map((e) => String(e.sequenceId)))];
3893
+ const sequences = await this.sequences.findAll({ _id: { $in: sequenceIds } }, ctx);
3894
+ const sequencesById = new Map(sequences.map((s) => [String(s._id), s]));
3895
+ const exited = [];
3896
+ for (const enrollment of active) {
3897
+ if (sequencesById.get(String(enrollment.sequenceId))?.replyDetectionEnabled === false) continue;
3898
+ try {
3899
+ exited.push(await this.exit(String(enrollment._id), "reply", ctx));
3900
+ } catch (err) {
3901
+ if (err instanceof IllegalTransitionError || err instanceof InvalidEnrollmentTransitionError || err instanceof NotFoundError) continue;
3902
+ throw err;
3903
+ }
3904
+ }
3905
+ return exited;
3906
+ }
3907
+ async _transition(id, target, ctx, patch = {}) {
3908
+ const existing = await this.getById(id, ctx);
3909
+ if (!existing) throw new NotFoundError("Enrollment", id);
3910
+ ENROLLMENT_MACHINE.assertTransition(id, existing.status, target);
3911
+ const entry = {
3912
+ status: target,
3913
+ occurredAt: /* @__PURE__ */ new Date(),
3914
+ ...ctx?.actorId ? { by: String(ctx.actorId) } : {}
3915
+ };
3916
+ const updated = await assertAndClaim(ENROLLMENT_MACHINE, this, id, {
3917
+ from: existing.status,
3918
+ to: target,
3919
+ patch: {
3920
+ ...patch,
3921
+ statusHistory: [...existing.statusHistory ?? [], entry]
3922
+ },
3923
+ options: ctx
3924
+ });
3925
+ if (!updated) throw new InvalidEnrollmentTransitionError({
3926
+ from: (await this.getById(id, ctx))?.status ?? existing.status,
3927
+ to: target,
3928
+ enrollmentId: id
3929
+ });
3930
+ return updated;
3931
+ }
3932
+ };
3933
+ //#endregion
3194
3934
  //#region src/repositories/create-repositories.ts
3195
3935
  /**
3196
3936
  * Repository factory — composes the shared mongokit plugin stack and
@@ -3224,6 +3964,7 @@ function createCrmRepositories(models, deps) {
3224
3964
  const account = new AccountRepository(models.Account, plugins, { events }, pagination);
3225
3965
  const campaign = new CampaignRepository(models.Campaign, plugins, { events }, pagination);
3226
3966
  const subscription = new SubscriptionRepository(models.Subscription, plugins, { events }, pagination);
3967
+ const suppression = new SuppressionRepository(models.Suppression, plugins, { events }, pagination);
3227
3968
  const activity = new ActivityRepository(models.Activity, plugins, { events }, pagination);
3228
3969
  const note = new NoteRepository(models.Note, plugins, { events }, pagination);
3229
3970
  const lossReason = new LossReasonRepository(models.LossReason, plugins, { events }, pagination);
@@ -3233,6 +3974,11 @@ function createCrmRepositories(models, deps) {
3233
3974
  events,
3234
3975
  conversations: conversation
3235
3976
  }, pagination);
3977
+ const sequence = new SequenceRepository(models.Sequence, plugins, { events }, pagination);
3978
+ const enrollment = new EnrollmentRepository(models.Enrollment, plugins, {
3979
+ events,
3980
+ sequences: sequence
3981
+ }, pagination);
3236
3982
  return Object.freeze({
3237
3983
  pipeline,
3238
3984
  lead,
@@ -3241,15 +3987,90 @@ function createCrmRepositories(models, deps) {
3241
3987
  account,
3242
3988
  campaign,
3243
3989
  subscription,
3990
+ suppression,
3244
3991
  activity,
3245
3992
  note,
3246
3993
  lossReason,
3247
3994
  emailEvent,
3248
3995
  conversation,
3249
- message
3996
+ message,
3997
+ sequence,
3998
+ enrollment
3250
3999
  });
3251
4000
  }
3252
4001
  //#endregion
4002
+ //#region src/events/dispatch.ts
4003
+ /**
4004
+ * Context key for the post-commit event queue. `Symbol.for` so the host
4005
+ * and the package agree on the key even across duplicated installs.
4006
+ */
4007
+ const PENDING_EVENTS = Symbol.for("@classytic/crm:pendingEvents");
4008
+ /**
4009
+ * EventTransport-compatible wrapper the engine hands to repositories.
4010
+ * `publish`/`subscribe` delegate straight to the underlying transport
4011
+ * (host-facing surface unchanged); domain verbs call `dispatch(ctx, event)`
4012
+ * so session/outbox/queue routing happens in ONE place.
4013
+ */
4014
+ var CrmEventDispatcher = class {
4015
+ transport;
4016
+ outbox;
4017
+ logger;
4018
+ constructor(options) {
4019
+ this.transport = options.transport;
4020
+ this.outbox = options.outbox;
4021
+ this.logger = options.logger;
4022
+ }
4023
+ get name() {
4024
+ return this.transport.name;
4025
+ }
4026
+ /** Raw immediate publish — post-commit flushes and host-side use only. */
4027
+ publish(event) {
4028
+ return this.transport.publish(event);
4029
+ }
4030
+ subscribe(pattern, handler) {
4031
+ return this.transport.subscribe(pattern, handler);
4032
+ }
4033
+ async close() {
4034
+ await this.transport.close?.();
4035
+ }
4036
+ /**
4037
+ * Session-bound dispatch (see module doc for the full contract).
4038
+ * `ctx` comes first so call sites read `dispatch(ctx, createEvent(...))`.
4039
+ */
4040
+ async dispatch(ctx, event) {
4041
+ const pending = ctx?.[PENDING_EVENTS];
4042
+ const session = ctx?.session;
4043
+ if (this.outbox) {
4044
+ const saveOptions = {};
4045
+ if (session !== void 0) saveOptions.session = session;
4046
+ await this.outbox.save(event, saveOptions);
4047
+ } else if (session && !pending) throw new UnmanagedSessionError(event.type);
4048
+ if (pending) {
4049
+ pending.push(event);
4050
+ return;
4051
+ }
4052
+ if (!session) try {
4053
+ await this.transport.publish(event);
4054
+ } catch (err) {
4055
+ this.logger?.error?.(`[crm] publish failed for ${event.type}`, { err });
4056
+ }
4057
+ }
4058
+ };
4059
+ /**
4060
+ * Drain a `[PENDING_EVENTS]` queue through a transport after commit,
4061
+ * swallowing per-event errors (durable delivery is the outbox's job).
4062
+ * Hosts composing their own outer transaction call this after commit;
4063
+ * most should use `engine.withTransaction(ctx, fn)` instead.
4064
+ */
4065
+ async function flushPending(transport, events, logger) {
4066
+ if (!transport || events.length === 0) return;
4067
+ for (const event of events) try {
4068
+ await transport.publish(event);
4069
+ } catch (err) {
4070
+ (logger?.error ?? console.error)(`[crm] publish failed for ${event.type}`, { err });
4071
+ }
4072
+ }
4073
+ //#endregion
3253
4074
  //#region src/events/in-process-bus.ts
3254
4075
  /**
3255
4076
  * In-process event bus — default fallback when no `eventTransport` is
@@ -3305,6 +4126,42 @@ function assertCrmCapabilities(probe, options = {}) {
3305
4126
  }
3306
4127
  }
3307
4128
  //#endregion
4129
+ //#region src/engine/run-unit.ts
4130
+ /**
4131
+ * runUnit — transaction + post-commit-publish discipline for CRM verbs
4132
+ * composed into one atomic unit (the canonical case: `LeadService.convert`
4133
+ * = Account + Contact + Opportunity + lead transition). Same contract as
4134
+ * `@classytic/refit` / `@classytic/review` / `@classytic/cart` /
4135
+ * `@classytic/invoice` (PACKAGE_RULES §P8.1):
4136
+ *
4137
+ * 1. Events dispatched inside the unit are QUEUED (`ctx[PENDING_EVENTS]`)
4138
+ * and flushed only after the transaction commits (queue resets on
4139
+ * retry attempts) — a rollback can't leak ghost events to in-process
4140
+ * subscribers. Outbox rows stay inside the transaction — the relay
4141
+ * reads only committed rows.
4142
+ * 2. A caller-owned `ctx.session` must arrive WITH a queue or an outbox —
4143
+ * `dispatch()` throws `UnmanagedSessionError` otherwise.
4144
+ */
4145
+ async function runUnit(config, ctx, body) {
4146
+ const internalCtx = ctx;
4147
+ const externalQueue = internalCtx[PENDING_EVENTS];
4148
+ if (ctx.session) return body(externalQueue ? {
4149
+ ...internalCtx,
4150
+ [PENDING_EVENTS]: externalQueue
4151
+ } : internalCtx);
4152
+ const pending = [];
4153
+ const result = await withTransaction(config.connection, async (session) => {
4154
+ pending.length = 0;
4155
+ return body({
4156
+ ...internalCtx,
4157
+ session,
4158
+ [PENDING_EVENTS]: pending
4159
+ });
4160
+ }, config.allowNonTransactional ? { allowFallback: true } : {});
4161
+ await flushPending(config.events, pending, config.logger);
4162
+ return result;
4163
+ }
4164
+ //#endregion
3308
4165
  //#region src/engine/create-crm.ts
3309
4166
  async function createCrm(config) {
3310
4167
  if (!config.connection) throw new Error("[crm] CrmConfig.connection is required");
@@ -3318,7 +4175,11 @@ async function createCrm(config) {
3318
4175
  ...config.pagination !== void 0 ? { pagination: config.pagination } : {},
3319
4176
  plugins: config.plugins ?? []
3320
4177
  };
3321
- const events = config.eventTransport ?? new InProcessCrmBus(config.logger ? { logger: config.logger } : void 0);
4178
+ const events = new CrmEventDispatcher({
4179
+ transport: config.eventTransport ?? new InProcessCrmBus(config.logger ? { logger: config.logger } : void 0),
4180
+ outbox: config.outbox,
4181
+ logger: config.logger
4182
+ });
3322
4183
  const models = createCrmModels(config.connection, resolved);
3323
4184
  const repositories = createCrmRepositories(models, {
3324
4185
  events,
@@ -3333,11 +4194,19 @@ async function createCrm(config) {
3333
4194
  repositories,
3334
4195
  events,
3335
4196
  config: Object.freeze(resolved),
4197
+ withTransaction(ctx, body) {
4198
+ return runUnit({
4199
+ connection: config.connection,
4200
+ events,
4201
+ logger: config.logger,
4202
+ allowNonTransactional: config.allowNonTransactional ?? false
4203
+ }, ctx, body);
4204
+ },
3336
4205
  async syncIndexes() {
3337
4206
  await Promise.all(Object.values(models).map((m) => m.syncIndexes()));
3338
4207
  },
3339
4208
  async dispose() {
3340
- await events.close?.();
4209
+ await events.close();
3341
4210
  }
3342
4211
  });
3343
4212
  }
@@ -3429,7 +4298,7 @@ var LeadService = class LeadService {
3429
4298
  opportunityId: String(opportunity._id),
3430
4299
  ...input.by !== void 0 ? { by: input.by } : {}
3431
4300
  }, ctx);
3432
- await this.events.publish(createEvent(CRM_EVENTS.LEAD_CONVERTED, {
4301
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.LEAD_CONVERTED, {
3433
4302
  leadId,
3434
4303
  contactId: String(contact._id),
3435
4304
  accountId: account ? String(account._id) : void 0,
@@ -3954,6 +4823,22 @@ const SubscriptionComplained = defineCrmEvent({
3954
4823
  campaignId: z.string()
3955
4824
  })
3956
4825
  });
4826
+ const SuppressionAdded = defineCrmEvent({
4827
+ name: CRM_EVENTS.SUPPRESSION_ADDED,
4828
+ resource: "suppression",
4829
+ 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.",
4830
+ zodSchema: z.object({
4831
+ email: z.string(),
4832
+ reason: z.enum(SUPPRESSION_REASONS),
4833
+ source: z.string().optional()
4834
+ })
4835
+ });
4836
+ const SuppressionRemoved = defineCrmEvent({
4837
+ name: CRM_EVENTS.SUPPRESSION_REMOVED,
4838
+ resource: "suppression",
4839
+ description: "unsuppress() removed an email from the org-wide suppression list.",
4840
+ zodSchema: z.object({ email: z.string() })
4841
+ });
3957
4842
  const emailEventSchema = z.object({
3958
4843
  eventId: z.string(),
3959
4844
  kind: z.string(),
@@ -4054,6 +4939,82 @@ const MessageSent = defineCrmEvent({
4054
4939
  externalMessageId: z.string().optional()
4055
4940
  })
4056
4941
  });
4942
+ const SequenceCreated = defineCrmEvent({
4943
+ name: CRM_EVENTS.SEQUENCE_CREATED,
4944
+ resource: "sequence",
4945
+ 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.",
4946
+ zodSchema: z.object({
4947
+ sequenceId: z.string(),
4948
+ name: z.string().optional()
4949
+ })
4950
+ });
4951
+ const SequenceActivated = defineCrmEvent({
4952
+ name: CRM_EVENTS.SEQUENCE_ACTIVATED,
4953
+ resource: "sequence",
4954
+ description: "activate() moved a sequence draft/paused → active.",
4955
+ zodSchema: z.object({ sequenceId: z.string() })
4956
+ });
4957
+ const SequencePaused = defineCrmEvent({
4958
+ name: CRM_EVENTS.SEQUENCE_PAUSED,
4959
+ resource: "sequence",
4960
+ description: "pause() suspended an active sequence (existing enrollments hold).",
4961
+ zodSchema: z.object({ sequenceId: z.string() })
4962
+ });
4963
+ const SequenceArchived = defineCrmEvent({
4964
+ name: CRM_EVENTS.SEQUENCE_ARCHIVED,
4965
+ resource: "sequence",
4966
+ description: "archive() retired a sequence — no new enrollments.",
4967
+ zodSchema: z.object({ sequenceId: z.string() })
4968
+ });
4969
+ const EnrollmentCreated = defineCrmEvent({
4970
+ name: CRM_EVENTS.ENROLLMENT_CREATED,
4971
+ resource: "enrollment",
4972
+ description: "enroll() attached a subject to a sequence.",
4973
+ zodSchema: z.object({
4974
+ enrollmentId: z.string(),
4975
+ sequenceId: z.string(),
4976
+ subjectId: z.string(),
4977
+ subjectKind: z.string()
4978
+ })
4979
+ });
4980
+ const EnrollmentStepSent = defineCrmEvent({
4981
+ name: CRM_EVENTS.ENROLLMENT_STEP_SENT,
4982
+ resource: "enrollment",
4983
+ 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.",
4984
+ zodSchema: z.object({
4985
+ enrollmentId: z.string(),
4986
+ sequenceId: z.string().optional(),
4987
+ stepIndex: z.number(),
4988
+ channel: z.string().optional()
4989
+ })
4990
+ });
4991
+ const EnrollmentPaused = defineCrmEvent({
4992
+ name: CRM_EVENTS.ENROLLMENT_PAUSED,
4993
+ resource: "enrollment",
4994
+ description: "pause() held an active enrollment.",
4995
+ zodSchema: z.object({ enrollmentId: z.string() })
4996
+ });
4997
+ const EnrollmentResumed = defineCrmEvent({
4998
+ name: CRM_EVENTS.ENROLLMENT_RESUMED,
4999
+ resource: "enrollment",
5000
+ description: "resume() reactivated a paused enrollment.",
5001
+ zodSchema: z.object({ enrollmentId: z.string() })
5002
+ });
5003
+ const EnrollmentCompleted = defineCrmEvent({
5004
+ name: CRM_EVENTS.ENROLLMENT_COMPLETED,
5005
+ resource: "enrollment",
5006
+ description: "complete() finished the final step of an enrollment.",
5007
+ zodSchema: z.object({ enrollmentId: z.string() })
5008
+ });
5009
+ const EnrollmentExited = defineCrmEvent({
5010
+ name: CRM_EVENTS.ENROLLMENT_EXITED,
5011
+ resource: "enrollment",
5012
+ description: "exit() removed a subject before completion (reply/unsubscribe/manual/goal).",
5013
+ zodSchema: z.object({
5014
+ enrollmentId: z.string(),
5015
+ exitReason: z.string().optional()
5016
+ })
5017
+ });
4057
5018
  const crmEventDefinitions = [
4058
5019
  LeadCreated,
4059
5020
  LeadStatusChanged,
@@ -4100,6 +5061,8 @@ const crmEventDefinitions = [
4100
5061
  SubscriptionUnsubscribed,
4101
5062
  SubscriptionBounced,
4102
5063
  SubscriptionComplained,
5064
+ SuppressionAdded,
5065
+ SuppressionRemoved,
4103
5066
  EmailEventOpened,
4104
5067
  EmailEventClicked,
4105
5068
  EmailEventBounced,
@@ -4110,17 +5073,34 @@ const crmEventDefinitions = [
4110
5073
  ConversationResolved,
4111
5074
  ConversationReopened,
4112
5075
  MessageReceived,
4113
- MessageSent
5076
+ MessageSent,
5077
+ SequenceCreated,
5078
+ SequenceActivated,
5079
+ SequencePaused,
5080
+ SequenceArchived,
5081
+ EnrollmentCreated,
5082
+ EnrollmentStepSent,
5083
+ EnrollmentPaused,
5084
+ EnrollmentResumed,
5085
+ EnrollmentCompleted,
5086
+ EnrollmentExited
4114
5087
  ];
4115
5088
  //#endregion
4116
5089
  //#region src/shared/calculators/forecast.ts
4117
5090
  /**
5091
+ * Pure forecast calculators — no I/O, no Mongo. Hosts call these after
5092
+ * loading opportunities via `repositories.opportunity.list(...)`.
5093
+ *
5094
+ * Amount is flat (`amount` Number + `currency` String per PACKAGE_RULES
5095
+ * §4) — same shape the opportunity schema stores.
5096
+ */
5097
+ /**
4118
5098
  * Aggregate opportunities into a forecast table — one row per bucket.
4119
5099
  * Currency mismatch within a bucket throws — partition by currency
4120
5100
  * upstream when multi-currency.
4121
5101
  */
4122
5102
  function forecast(opportunities, options = {}) {
4123
- const bucketFn = options.bucket ?? defaultBucket;
5103
+ const bucketFn = options.bucket ?? (options.timeZone !== void 0 ? zoneBucket(options.timeZone) : defaultBucket);
4124
5104
  const include = options.include ?? defaultInclude;
4125
5105
  const rows = /* @__PURE__ */ new Map();
4126
5106
  for (const o of opportunities) {
@@ -4153,6 +5133,10 @@ function defaultBucket(o) {
4153
5133
  const d = o.expectedCloseAt;
4154
5134
  return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}`;
4155
5135
  }
5136
+ /** Month bucket cut at the business's local midnight, not UTC. */
5137
+ function zoneBucket(timeZone) {
5138
+ return (o) => civilDateOf(o.expectedCloseAt, timeZone).slice(0, 7);
5139
+ }
4156
5140
  /**
4157
5141
  * "Where do deals die?" summary — opportunities partitioned across
4158
5142
  * stages with current count + won/lost-from-here.
@@ -4189,7 +5173,7 @@ const objectIdLike = z.union([z.string().min(1), z.string()]);
4189
5173
  const metadataSchema = z.record(z.string(), z.unknown()).optional();
4190
5174
  const tagsSchema = z.array(z.string().min(1).max(64)).max(50).optional();
4191
5175
  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");
5176
+ const currencyCodeSchema = z.string().regex(CURRENCY_PATTERN, "currency must be an ISO 4217 code (3 uppercase letters)");
4193
5177
  //#endregion
4194
5178
  //#region src/validators/pipeline.schema.ts
4195
5179
  const stageInputSchema = z.object({
@@ -4243,7 +5227,7 @@ const convertLeadSchema = z.object({
4243
5227
  pipelineId: objectIdLike,
4244
5228
  opportunityName: z.string().min(1).max(200).optional(),
4245
5229
  amount: z.number().nonnegative().optional(),
4246
- currency: z.string().regex(/^[A-Z]{3}$/).optional(),
5230
+ currency: currencyCodeSchema.optional(),
4247
5231
  expectedCloseAt: z.coerce.date().optional(),
4248
5232
  by: z.string().optional()
4249
5233
  });
@@ -4399,6 +5383,21 @@ const subscriptionFilterSchema = z.object({
4399
5383
  campaignId: objectIdLike.optional()
4400
5384
  });
4401
5385
  //#endregion
5386
+ //#region src/validators/suppression.schema.ts
5387
+ const suppressSchema = z.object({
5388
+ email: z.string().email().max(320),
5389
+ reason: z.enum(SUPPRESSION_REASONS),
5390
+ source: z.string().max(200).optional(),
5391
+ subjectKind: z.string().max(50).optional(),
5392
+ subjectId: z.string().max(100).optional(),
5393
+ metadata: metadataSchema
5394
+ });
5395
+ const unsuppressSchema = z.object({ email: z.string().email().max(320) });
5396
+ const suppressionFilterSchema = z.object({
5397
+ email: z.string().max(320).optional(),
5398
+ reason: z.enum(SUPPRESSION_REASONS).optional()
5399
+ });
5400
+ //#endregion
4402
5401
  //#region src/validators/activity.schema.ts
4403
5402
  const planActivitySchema = z.object({
4404
5403
  type: z.enum(ACTIVITY_TYPES),
@@ -4475,6 +5474,54 @@ const emailEventFilterSchema = z.object({
4475
5474
  messageId: z.string().optional()
4476
5475
  });
4477
5476
  //#endregion
5477
+ //#region src/validators/sequence.schema.ts
5478
+ const sendEmailStepSchema = z.object({
5479
+ id: z.string().min(1),
5480
+ type: z.literal("SEND_EMAIL"),
5481
+ name: z.string().max(100).optional(),
5482
+ subject: z.string().min(1).max(300),
5483
+ htmlBody: z.string().min(1),
5484
+ fromName: z.string().max(100).optional(),
5485
+ fromEmail: z.string().email().optional()
5486
+ });
5487
+ const delayStepSchema = z.object({
5488
+ id: z.string().min(1),
5489
+ type: z.literal("DELAY"),
5490
+ name: z.string().max(100).optional(),
5491
+ settings: z.object({
5492
+ days: z.number().int().min(0).optional(),
5493
+ hours: z.number().int().min(0).optional(),
5494
+ minutes: z.number().int().min(0).optional()
5495
+ })
5496
+ });
5497
+ const sequenceStepSchema = z.discriminatedUnion("type", [sendEmailStepSchema, delayStepSchema]);
5498
+ const createSequenceSchema = z.object({
5499
+ name: z.string().min(1).max(200),
5500
+ description: z.string().max(1e3).optional(),
5501
+ fromName: z.string().max(100).optional(),
5502
+ fromEmail: z.string().email().optional(),
5503
+ steps: z.array(sequenceStepSchema).default([]),
5504
+ replyDetectionEnabled: z.boolean().default(true),
5505
+ tags: tagsSchema,
5506
+ ownerId: z.string().optional(),
5507
+ metadata: metadataSchema
5508
+ });
5509
+ const updateSequenceSchema = createSequenceSchema.partial();
5510
+ const sequenceFilterSchema = z.object({
5511
+ status: z.enum([
5512
+ "draft",
5513
+ "active",
5514
+ "paused",
5515
+ "archived"
5516
+ ]).optional(),
5517
+ ownerId: z.string().optional(),
5518
+ tags: z.string().optional(),
5519
+ search: z.string().optional(),
5520
+ page: z.coerce.number().int().min(1).optional(),
5521
+ limit: z.coerce.number().int().min(1).max(200).optional(),
5522
+ sort: z.string().optional()
5523
+ });
5524
+ //#endregion
4478
5525
  //#region src/validators/index.ts
4479
5526
  var validators_exports = /* @__PURE__ */ __exportAll({
4480
5527
  accountFilterSchema: () => accountFilterSchema,
@@ -4494,6 +5541,7 @@ var validators_exports = /* @__PURE__ */ __exportAll({
4494
5541
  createLeadSchema: () => createLeadSchema,
4495
5542
  createLossReasonSchema: () => createLossReasonSchema,
4496
5543
  createPipelineSchema: () => createPipelineSchema,
5544
+ createSequenceSchema: () => createSequenceSchema,
4497
5545
  currencyCodeSchema: () => currencyCodeSchema,
4498
5546
  emailEventFilterSchema: () => emailEventFilterSchema,
4499
5547
  leadFilterSchema: () => leadFilterSchema,
@@ -4508,11 +5556,16 @@ var validators_exports = /* @__PURE__ */ __exportAll({
4508
5556
  pipelineFilterSchema: () => pipelineFilterSchema,
4509
5557
  planActivitySchema: () => planActivitySchema,
4510
5558
  recordEmailEventSchema: () => recordEmailEventSchema,
5559
+ sequenceFilterSchema: () => sequenceFilterSchema,
5560
+ sequenceStepSchema: () => sequenceStepSchema,
4511
5561
  stageInputSchemaExport: () => stageInputSchemaExport,
4512
5562
  subscribeSchema: () => subscribeSchema,
4513
5563
  subscriptionFilterSchema: () => subscriptionFilterSchema,
5564
+ suppressSchema: () => suppressSchema,
5565
+ suppressionFilterSchema: () => suppressionFilterSchema,
4514
5566
  tagsSchema: () => tagsSchema,
4515
5567
  unsubscribeSchema: () => unsubscribeSchema,
5568
+ unsuppressSchema: () => unsuppressSchema,
4516
5569
  updateAccountSchema: () => updateAccountSchema,
4517
5570
  updateActivitySchema: () => updateActivitySchema,
4518
5571
  updateCampaignSchema: () => updateCampaignSchema,
@@ -4520,7 +5573,8 @@ var validators_exports = /* @__PURE__ */ __exportAll({
4520
5573
  updateLeadSchema: () => updateLeadSchema,
4521
5574
  updateLossReasonSchema: () => updateLossReasonSchema,
4522
5575
  updateNoteSchema: () => updateNoteSchema,
4523
- updatePipelineSchema: () => updatePipelineSchema
5576
+ updatePipelineSchema: () => updatePipelineSchema,
5577
+ updateSequenceSchema: () => updateSequenceSchema
4524
5578
  });
4525
5579
  //#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 };
5580
+ 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 };