@classytic/crm 0.5.0 → 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
- import { t as __exportAll } from "./rolldown-runtime.js";
2
- import { AccountAssigned, AccountCreated, AccountEnriched, AccountMerged, AccountUpdated, ActivityCancelled, ActivityCompleted, ActivityOutcomeRecorded, ActivityPlanned, ActivityRescheduled, CRM_EVENTS, CampaignActivated, CampaignArchived, CampaignCompleted, CampaignCreated, CampaignPaused, CampaignResumed, ContactAssigned, ContactCreated, ContactEnriched, ContactMerged, ContactUpdated, ConversationAssigned, ConversationCreated, ConversationReopened, ConversationResolved, EmailEventBounced, EmailEventClicked, EmailEventComplained, EmailEventOpened, EmailEventUnsubscribed, InProcessCrmBus, LeadAssigned, LeadConverted, LeadCreated, LeadDisqualified, LeadEnriched, LeadScored, LeadStatusChanged, MessageReceived, MessageSent, NoteAdded, OpportunityAbandoned, OpportunityAmountChanged, OpportunityAssigned, OpportunityCreated, OpportunityExtended, OpportunityLost, OpportunityStageAdvanced, OpportunityStageRegressed, OpportunityWon, PipelineArchived, PipelineCreated, SubscriptionBounced, SubscriptionComplained, SubscriptionConfirmed, SubscriptionCreated, SubscriptionUnsubscribed, createEvent, crmEventDefinitions } from "./events.js";
1
+ import { t as __exportAll } from "./chunk.js";
3
2
  import { Schema } from "mongoose";
4
- import { Repository, aggregateHelpersPlugin, isDuplicateKeyError, methodRegistryPlugin, multiTenantPlugin } from "@classytic/mongokit";
5
- import { assertAndClaim, defineStateMachine } from "@classytic/primitives/state-machine";
3
+ import { Repository, aggregateHelpersPlugin, isDuplicateKeyError, methodRegistryPlugin, multiTenantPlugin, withTransaction } from "@classytic/mongokit";
4
+ import { createEvent as createEvent$1, matchEventPattern } from "@classytic/primitives/events";
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,107 @@ 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))
2015
+ });
2016
+ }
2017
+ //#endregion
2018
+ //#region src/events/event-constants.ts
2019
+ /** PACKAGE_RULES rule 12: `package:resource.verb`. */
2020
+ const CRM_EVENTS = {
2021
+ LEAD_CREATED: "crm:lead.created",
2022
+ LEAD_STATUS_CHANGED: "crm:lead.status_changed",
2023
+ LEAD_SCORED: "crm:lead.scored",
2024
+ LEAD_CONVERTED: "crm:lead.converted",
2025
+ LEAD_DISQUALIFIED: "crm:lead.disqualified",
2026
+ ACCOUNT_CREATED: "crm:account.created",
2027
+ ACCOUNT_UPDATED: "crm:account.updated",
2028
+ CONTACT_CREATED: "crm:contact.created",
2029
+ CONTACT_UPDATED: "crm:contact.updated",
2030
+ OPPORTUNITY_CREATED: "crm:opportunity.created",
2031
+ OPPORTUNITY_STAGE_ADVANCED: "crm:opportunity.stage_advanced",
2032
+ OPPORTUNITY_STAGE_REGRESSED: "crm:opportunity.stage_regressed",
2033
+ OPPORTUNITY_WON: "crm:opportunity.won",
2034
+ OPPORTUNITY_LOST: "crm:opportunity.lost",
2035
+ OPPORTUNITY_ABANDONED: "crm:opportunity.abandoned",
2036
+ PIPELINE_CREATED: "crm:pipeline.created",
2037
+ PIPELINE_ARCHIVED: "crm:pipeline.archived",
2038
+ ACTIVITY_PLANNED: "crm:activity.planned",
2039
+ ACTIVITY_COMPLETED: "crm:activity.completed",
2040
+ ACTIVITY_CANCELLED: "crm:activity.cancelled",
2041
+ NOTE_ADDED: "crm:note.added",
2042
+ CAMPAIGN_CREATED: "crm:campaign.created",
2043
+ CAMPAIGN_ACTIVATED: "crm:campaign.activated",
2044
+ CAMPAIGN_PAUSED: "crm:campaign.paused",
2045
+ CAMPAIGN_RESUMED: "crm:campaign.resumed",
2046
+ CAMPAIGN_COMPLETED: "crm:campaign.completed",
2047
+ CAMPAIGN_ARCHIVED: "crm:campaign.archived",
2048
+ SUBSCRIPTION_CREATED: "crm:subscription.created",
2049
+ SUBSCRIPTION_CONFIRMED: "crm:subscription.confirmed",
2050
+ SUBSCRIPTION_UNSUBSCRIBED: "crm:subscription.unsubscribed",
2051
+ SUBSCRIPTION_BOUNCED: "crm:subscription.bounced",
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",
2063
+ EMAIL_EVENT_OPENED: "crm:email_event.opened",
2064
+ EMAIL_EVENT_CLICKED: "crm:email_event.clicked",
2065
+ EMAIL_EVENT_BOUNCED: "crm:email_event.bounced",
2066
+ EMAIL_EVENT_COMPLAINED: "crm:email_event.complained",
2067
+ EMAIL_EVENT_UNSUBSCRIBED: "crm:email_event.unsubscribed",
2068
+ LEAD_ENRICHED: "crm:lead.enriched",
2069
+ LEAD_ASSIGNED: "crm:lead.assigned",
2070
+ CONTACT_ENRICHED: "crm:contact.enriched",
2071
+ CONTACT_ASSIGNED: "crm:contact.assigned",
2072
+ CONTACT_MERGED: "crm:contact.merged",
2073
+ ACCOUNT_ENRICHED: "crm:account.enriched",
2074
+ ACCOUNT_ASSIGNED: "crm:account.assigned",
2075
+ ACCOUNT_MERGED: "crm:account.merged",
2076
+ OPPORTUNITY_ASSIGNED: "crm:opportunity.assigned",
2077
+ OPPORTUNITY_AMOUNT_CHANGED: "crm:opportunity.amount_changed",
2078
+ OPPORTUNITY_EXTENDED: "crm:opportunity.extended",
2079
+ ACTIVITY_RESCHEDULED: "crm:activity.rescheduled",
2080
+ ACTIVITY_OUTCOME_RECORDED: "crm:activity.outcome_recorded",
2081
+ SUPPRESSION_ADDED: "crm:suppression.added",
2082
+ SUPPRESSION_REMOVED: "crm:suppression.removed",
2083
+ CONVERSATION_CREATED: "crm:conversation.created",
2084
+ CONVERSATION_ASSIGNED: "crm:conversation.assigned",
2085
+ CONVERSATION_RESOLVED: "crm:conversation.resolved",
2086
+ CONVERSATION_REOPENED: "crm:conversation.reopened",
2087
+ MESSAGE_RECEIVED: "crm:message.received",
2088
+ MESSAGE_SENT: "crm:message.sent"
2089
+ };
2090
+ //#endregion
2091
+ //#region src/events/helpers.ts
2092
+ /**
2093
+ * Event creation helper.
2094
+ *
2095
+ * Thin wrapper around `@classytic/primitives`' `createEvent` that fills the
2096
+ * shared `EventMeta` from a `CrmContext` (userId/actorId, organizationId,
2097
+ * correlationId). Use this instead of importing `createEvent` directly so
2098
+ * the `source: 'crm'` stamp + tenant scoping stay consistent.
2099
+ */
2100
+ function createEvent(type, payload, ctx, meta) {
2101
+ return createEvent$1(type, payload, {
2102
+ source: "crm",
2103
+ ...ctx?.actorId !== void 0 ? { actorId: String(ctx.actorId) } : {},
2104
+ ...ctx?.organizationId !== void 0 ? { organizationId: String(ctx.organizationId) } : {},
2105
+ ...ctx?.correlationId !== void 0 ? { correlationId: ctx.correlationId } : {},
2106
+ ...meta
1653
2107
  });
1654
2108
  }
1655
2109
  //#endregion
@@ -1698,7 +2152,7 @@ var PipelineRepository = class extends Repository {
1698
2152
  async archive(id, ctx) {
1699
2153
  const updated = await this.update(id, { isArchived: true }, ctx);
1700
2154
  if (!updated) throw new NotFoundError("Pipeline", id);
1701
- 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));
1702
2156
  return updated;
1703
2157
  }
1704
2158
  };
@@ -1764,9 +2218,27 @@ const ACTIVITY_MACHINE = defineStateMachine({
1764
2218
  activityId: entityId
1765
2219
  })
1766
2220
  });
1767
- //#endregion
1768
- //#region src/shared/calculators/scoring.ts
1769
- /**
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
+ });
2239
+ //#endregion
2240
+ //#region src/shared/calculators/scoring.ts
2241
+ /**
1770
2242
  * Reference implementation. Intentionally simple — hosts read it and
1771
2243
  * replace it.
1772
2244
  *
@@ -1833,12 +2305,12 @@ var LeadRepository = class extends Repository {
1833
2305
  let result = created;
1834
2306
  if (score !== 0) {
1835
2307
  result = await this.update(String(created._id), { score }, ctx) ?? created;
1836
- await this.events.publish(createEvent(CRM_EVENTS.LEAD_SCORED, {
2308
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.LEAD_SCORED, {
1837
2309
  leadId: String(created._id),
1838
2310
  score
1839
2311
  }, ctx));
1840
2312
  }
1841
- await this.events.publish(createEvent(CRM_EVENTS.LEAD_CREATED, {
2313
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.LEAD_CREATED, {
1842
2314
  leadId: String(result._id),
1843
2315
  email: result.email,
1844
2316
  score: result.score
@@ -1857,7 +2329,7 @@ var LeadRepository = class extends Repository {
1857
2329
  note: reason,
1858
2330
  patch: { disqualifyReason: reason }
1859
2331
  }, ctx);
1860
- await this.events.publish(createEvent(CRM_EVENTS.LEAD_DISQUALIFIED, {
2332
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.LEAD_DISQUALIFIED, {
1861
2333
  leadId: id,
1862
2334
  reason
1863
2335
  }, ctx));
@@ -1879,7 +2351,7 @@ var LeadRepository = class extends Repository {
1879
2351
  if (!await this.getById(id, ctx)) throw new NotFoundError("Lead", id);
1880
2352
  if (!await this.update(id, patch, ctx)) throw new NotFoundError("Lead", id);
1881
2353
  const fields = Object.keys(patch);
1882
- await this.events.publish(createEvent(CRM_EVENTS.LEAD_ENRICHED, {
2354
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.LEAD_ENRICHED, {
1883
2355
  leadId: id,
1884
2356
  fields,
1885
2357
  ...extras.provider !== void 0 ? { provider: extras.provider } : {}
@@ -1903,7 +2375,7 @@ var LeadRepository = class extends Repository {
1903
2375
  if (!current) throw new NotFoundError("Lead", id);
1904
2376
  return current;
1905
2377
  }
1906
- await this.events.publish(createEvent(CRM_EVENTS.LEAD_ASSIGNED, {
2378
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.LEAD_ASSIGNED, {
1907
2379
  leadId: id,
1908
2380
  ownerId,
1909
2381
  ...before.ownerId !== void 0 ? { previousOwnerId: before.ownerId } : {}
@@ -1921,7 +2393,7 @@ var LeadRepository = class extends Repository {
1921
2393
  if (score === lead.score) return lead;
1922
2394
  const updated = await this.update(id, { score }, ctx);
1923
2395
  if (!updated) throw new NotFoundError("Lead", id);
1924
- await this.events.publish(createEvent(CRM_EVENTS.LEAD_SCORED, {
2396
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.LEAD_SCORED, {
1925
2397
  leadId: id,
1926
2398
  score
1927
2399
  }, ctx));
@@ -1951,7 +2423,7 @@ var LeadRepository = class extends Repository {
1951
2423
  to: target,
1952
2424
  leadId: id
1953
2425
  });
1954
- await this.events.publish(createEvent(CRM_EVENTS.LEAD_STATUS_CHANGED, {
2426
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.LEAD_STATUS_CHANGED, {
1955
2427
  leadId: id,
1956
2428
  from: existing.status,
1957
2429
  to: target
@@ -2017,7 +2489,7 @@ var OpportunityRepository = class extends Repository {
2017
2489
  ...input.metadata !== void 0 ? { metadata: input.metadata } : {}
2018
2490
  };
2019
2491
  const created = await this.create(draft, ctx);
2020
- await this.events.publish(createEvent(CRM_EVENTS.OPPORTUNITY_CREATED, {
2492
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.OPPORTUNITY_CREATED, {
2021
2493
  opportunityId: String(created._id),
2022
2494
  pipelineId: input.pipelineId,
2023
2495
  stageId: String(stage._id)
@@ -2061,7 +2533,7 @@ var OpportunityRepository = class extends Repository {
2061
2533
  stageId: String(target._id)
2062
2534
  });
2063
2535
  const event = target.sequence > current.sequence ? CRM_EVENTS.OPPORTUNITY_STAGE_ADVANCED : CRM_EVENTS.OPPORTUNITY_STAGE_REGRESSED;
2064
- await this.events.publish(createEvent(event, {
2536
+ await this.events.dispatch(ctx, createEvent(event, {
2065
2537
  opportunityId: id,
2066
2538
  from: {
2067
2539
  stageId: String(current._id),
@@ -2083,7 +2555,7 @@ var OpportunityRepository = class extends Repository {
2083
2555
  ...input.by !== void 0 ? { by: input.by } : {},
2084
2556
  ...input.note !== void 0 ? { note: input.note } : {}
2085
2557
  }, ctx);
2086
- await this.events.publish(createEvent(CRM_EVENTS.OPPORTUNITY_WON, {
2558
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.OPPORTUNITY_WON, {
2087
2559
  opportunityId: id,
2088
2560
  amount: updated.amount
2089
2561
  }, ctx));
@@ -2098,7 +2570,7 @@ var OpportunityRepository = class extends Repository {
2098
2570
  ...input.by !== void 0 ? { by: input.by } : {},
2099
2571
  ...input.note !== void 0 ? { note: input.note } : {}
2100
2572
  }, ctx);
2101
- await this.events.publish(createEvent(CRM_EVENTS.OPPORTUNITY_LOST, {
2573
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.OPPORTUNITY_LOST, {
2102
2574
  opportunityId: id,
2103
2575
  lostReasonId: updated.lostReasonId
2104
2576
  }, ctx));
@@ -2121,7 +2593,7 @@ var OpportunityRepository = class extends Repository {
2121
2593
  if (!current) throw new NotFoundError("Opportunity", id);
2122
2594
  return current;
2123
2595
  }
2124
- await this.events.publish(createEvent(CRM_EVENTS.OPPORTUNITY_ASSIGNED, {
2596
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.OPPORTUNITY_ASSIGNED, {
2125
2597
  opportunityId: id,
2126
2598
  ownerId,
2127
2599
  ...before.ownerId !== void 0 ? { previousOwnerId: before.ownerId } : {}
@@ -2145,7 +2617,7 @@ var OpportunityRepository = class extends Repository {
2145
2617
  if (amount === existing.amount) return existing;
2146
2618
  const updated = await this.update(id, { amount }, ctx);
2147
2619
  if (!updated) throw new NotFoundError("Opportunity", id);
2148
- await this.events.publish(createEvent(CRM_EVENTS.OPPORTUNITY_AMOUNT_CHANGED, {
2620
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.OPPORTUNITY_AMOUNT_CHANGED, {
2149
2621
  opportunityId: id,
2150
2622
  amount,
2151
2623
  ...existing.amount !== void 0 ? { previousAmount: existing.amount } : {},
@@ -2172,7 +2644,7 @@ var OpportunityRepository = class extends Repository {
2172
2644
  if (previous && newDate <= previous) return existing;
2173
2645
  const updated = await this.update(id, { expectedCloseAt: newDate }, ctx);
2174
2646
  if (!updated) throw new NotFoundError("Opportunity", id);
2175
- await this.events.publish(createEvent(CRM_EVENTS.OPPORTUNITY_EXTENDED, {
2647
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.OPPORTUNITY_EXTENDED, {
2176
2648
  opportunityId: id,
2177
2649
  expectedCloseAt: newDate.toISOString(),
2178
2650
  ...previous ? { previousExpectedCloseAt: previous.toISOString() } : {},
@@ -2189,7 +2661,7 @@ var OpportunityRepository = class extends Repository {
2189
2661
  ...by !== void 0 ? { by } : {},
2190
2662
  ...note !== void 0 ? { note } : {}
2191
2663
  }, ctx);
2192
- 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));
2193
2665
  return updated;
2194
2666
  }
2195
2667
  async _closeStatus(id, target, patch, extras, ctx) {
@@ -2286,7 +2758,7 @@ var ContactRepository = class extends Repository {
2286
2758
  */
2287
2759
  async createContact(input, ctx) {
2288
2760
  const created = await this.create(input, ctx);
2289
- await this.events.publish(createEvent(CRM_EVENTS.CONTACT_CREATED, {
2761
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.CONTACT_CREATED, {
2290
2762
  contactId: String(created._id),
2291
2763
  email: created.contact?.email
2292
2764
  }, ctx));
@@ -2301,7 +2773,7 @@ var ContactRepository = class extends Repository {
2301
2773
  if (!await this.getById(id, ctx)) throw new NotFoundError("Contact", id);
2302
2774
  const updated = await this.update(id, patch, ctx);
2303
2775
  if (!updated) throw new NotFoundError("Contact", id);
2304
- await this.events.publish(createEvent(CRM_EVENTS.CONTACT_ENRICHED, {
2776
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.CONTACT_ENRICHED, {
2305
2777
  contactId: id,
2306
2778
  fields: Object.keys(patch),
2307
2779
  ...extras.provider !== void 0 ? { provider: extras.provider } : {}
@@ -2322,7 +2794,7 @@ var ContactRepository = class extends Repository {
2322
2794
  if (!current) throw new NotFoundError("Contact", id);
2323
2795
  return current;
2324
2796
  }
2325
- await this.events.publish(createEvent(CRM_EVENTS.CONTACT_ASSIGNED, {
2797
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.CONTACT_ASSIGNED, {
2326
2798
  contactId: id,
2327
2799
  ownerId,
2328
2800
  ...before.ownerId !== void 0 ? { previousOwnerId: before.ownerId } : {}
@@ -2372,7 +2844,7 @@ var ContactRepository = class extends Repository {
2372
2844
  const updated = await this.update(primaryId, patch, ctx);
2373
2845
  if (!updated) throw new NotFoundError("Contact", primaryId);
2374
2846
  await this.delete(secondaryId, ctx);
2375
- await this.events.publish(createEvent(CRM_EVENTS.CONTACT_MERGED, {
2847
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.CONTACT_MERGED, {
2376
2848
  primaryId,
2377
2849
  secondaryId,
2378
2850
  strategy
@@ -2404,7 +2876,7 @@ var AccountRepository = class extends Repository {
2404
2876
  }
2405
2877
  async createAccount(input, ctx) {
2406
2878
  const created = await this.create(input, ctx);
2407
- await this.events.publish(createEvent(CRM_EVENTS.ACCOUNT_CREATED, {
2879
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ACCOUNT_CREATED, {
2408
2880
  accountId: String(created._id),
2409
2881
  name: created.name
2410
2882
  }, ctx));
@@ -2414,7 +2886,7 @@ var AccountRepository = class extends Repository {
2414
2886
  if (!await this.getById(id, ctx)) throw new NotFoundError("Account", id);
2415
2887
  const updated = await this.update(id, patch, ctx);
2416
2888
  if (!updated) throw new NotFoundError("Account", id);
2417
- await this.events.publish(createEvent(CRM_EVENTS.ACCOUNT_ENRICHED, {
2889
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ACCOUNT_ENRICHED, {
2418
2890
  accountId: id,
2419
2891
  fields: Object.keys(patch),
2420
2892
  ...extras.provider !== void 0 ? { provider: extras.provider } : {}
@@ -2434,7 +2906,7 @@ var AccountRepository = class extends Repository {
2434
2906
  if (!current) throw new NotFoundError("Account", id);
2435
2907
  return current;
2436
2908
  }
2437
- await this.events.publish(createEvent(CRM_EVENTS.ACCOUNT_ASSIGNED, {
2909
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ACCOUNT_ASSIGNED, {
2438
2910
  accountId: id,
2439
2911
  ownerId,
2440
2912
  ...before.ownerId !== void 0 ? { previousOwnerId: before.ownerId } : {}
@@ -2463,7 +2935,7 @@ var AccountRepository = class extends Repository {
2463
2935
  const updated = await this.update(primaryId, patch, ctx);
2464
2936
  if (!updated) throw new NotFoundError("Account", primaryId);
2465
2937
  await this.delete(secondaryId, ctx);
2466
- await this.events.publish(createEvent(CRM_EVENTS.ACCOUNT_MERGED, {
2938
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ACCOUNT_MERGED, {
2467
2939
  primaryId,
2468
2940
  secondaryId,
2469
2941
  strategy
@@ -2507,7 +2979,7 @@ var CampaignRepository = class extends Repository {
2507
2979
  if (this.rejectDuplicates && input.name && isDuplicateKeyError(err)) throw new DuplicateCampaignError(input.name);
2508
2980
  throw err;
2509
2981
  }
2510
- await this.events.publish(createEvent(CRM_EVENTS.CAMPAIGN_CREATED, {
2982
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.CAMPAIGN_CREATED, {
2511
2983
  campaignId: String(created._id),
2512
2984
  name: created.name
2513
2985
  }, ctx));
@@ -2515,7 +2987,7 @@ var CampaignRepository = class extends Repository {
2515
2987
  }
2516
2988
  async activate(id, by, ctx) {
2517
2989
  const updated = await this._transition(id, "active", { ...by !== void 0 ? { by } : {} }, ctx);
2518
- 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));
2519
2991
  return updated;
2520
2992
  }
2521
2993
  async pause(id, reason, by, ctx) {
@@ -2523,7 +2995,7 @@ var CampaignRepository = class extends Repository {
2523
2995
  ...by !== void 0 ? { by } : {},
2524
2996
  ...reason !== void 0 ? { note: reason } : {}
2525
2997
  }, ctx);
2526
- await this.events.publish(createEvent(CRM_EVENTS.CAMPAIGN_PAUSED, {
2998
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.CAMPAIGN_PAUSED, {
2527
2999
  campaignId: id,
2528
3000
  reason
2529
3001
  }, ctx));
@@ -2531,13 +3003,13 @@ var CampaignRepository = class extends Repository {
2531
3003
  }
2532
3004
  async resume(id, by, ctx) {
2533
3005
  const updated = await this._transition(id, "active", { ...by !== void 0 ? { by } : {} }, ctx);
2534
- 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));
2535
3007
  return updated;
2536
3008
  }
2537
3009
  async complete(id, extras = {}, ctx) {
2538
3010
  const patch = extras.actualRevenueAmount !== void 0 ? { actualRevenueAmount: extras.actualRevenueAmount } : {};
2539
3011
  const updated = await this._transition(id, "completed", extras, ctx, patch);
2540
- await this.events.publish(createEvent(CRM_EVENTS.CAMPAIGN_COMPLETED, {
3012
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.CAMPAIGN_COMPLETED, {
2541
3013
  campaignId: id,
2542
3014
  actualRevenueAmount: updated.actualRevenueAmount
2543
3015
  }, ctx));
@@ -2545,7 +3017,7 @@ var CampaignRepository = class extends Repository {
2545
3017
  }
2546
3018
  async archive(id, by, ctx) {
2547
3019
  const updated = await this._transition(id, "archived", { ...by !== void 0 ? { by } : {} }, ctx);
2548
- 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));
2549
3021
  return updated;
2550
3022
  }
2551
3023
  async _transition(id, target, extras, ctx, patch = {}) {
@@ -2636,7 +3108,7 @@ var SubscriptionRepository = class extends Repository {
2636
3108
  });
2637
3109
  throw err;
2638
3110
  }
2639
- await this.events.publish(createEvent(CRM_EVENTS.SUBSCRIPTION_CREATED, {
3111
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.SUBSCRIPTION_CREATED, {
2640
3112
  subscriptionId: String(sub._id),
2641
3113
  contactId: sub.contactId,
2642
3114
  campaignId: sub.campaignId,
@@ -2649,7 +3121,7 @@ var SubscriptionRepository = class extends Repository {
2649
3121
  if (!sub || sub.status !== "pending") throw new InvalidConfirmationTokenError();
2650
3122
  const confirmedAt = /* @__PURE__ */ new Date();
2651
3123
  const updated = await this._transition(String(sub._id), "confirmed", { patch: { confirmedAt } }, ctx);
2652
- await this.events.publish(createEvent(CRM_EVENTS.SUBSCRIPTION_CONFIRMED, {
3124
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.SUBSCRIPTION_CONFIRMED, {
2653
3125
  subscriptionId: String(sub._id),
2654
3126
  contactId: sub.contactId,
2655
3127
  campaignId: sub.campaignId,
@@ -2669,7 +3141,7 @@ var SubscriptionRepository = class extends Repository {
2669
3141
  ...reason !== void 0 ? { unsubscribeReason: reason } : {}
2670
3142
  }
2671
3143
  }, ctx);
2672
- await this.events.publish(createEvent(CRM_EVENTS.SUBSCRIPTION_UNSUBSCRIBED, {
3144
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.SUBSCRIPTION_UNSUBSCRIBED, {
2673
3145
  subscriptionId: String(sub._id),
2674
3146
  contactId: sub.contactId,
2675
3147
  campaignId: sub.campaignId,
@@ -2689,7 +3161,7 @@ var SubscriptionRepository = class extends Repository {
2689
3161
  ...reason !== void 0 ? { bounceReason: reason } : {}
2690
3162
  }
2691
3163
  }, ctx);
2692
- await this.events.publish(createEvent(CRM_EVENTS.SUBSCRIPTION_BOUNCED, {
3164
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.SUBSCRIPTION_BOUNCED, {
2693
3165
  subscriptionId: id,
2694
3166
  contactId: existing.contactId,
2695
3167
  campaignId: existing.campaignId,
@@ -2703,7 +3175,7 @@ var SubscriptionRepository = class extends Repository {
2703
3175
  if (existing.status === "complained") return existing;
2704
3176
  const complainedAt = /* @__PURE__ */ new Date();
2705
3177
  const updated = await this._transition(id, "complained", { patch: { complainedAt } }, ctx);
2706
- await this.events.publish(createEvent(CRM_EVENTS.SUBSCRIPTION_COMPLAINED, {
3178
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.SUBSCRIPTION_COMPLAINED, {
2707
3179
  subscriptionId: id,
2708
3180
  contactId: existing.contactId,
2709
3181
  campaignId: existing.campaignId
@@ -2738,6 +3210,78 @@ var SubscriptionRepository = class extends Repository {
2738
3210
  }
2739
3211
  };
2740
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
2741
3285
  //#region src/repositories/activity.repository.ts
2742
3286
  var ActivityRepository = class extends Repository {
2743
3287
  events;
@@ -2751,7 +3295,7 @@ var ActivityRepository = class extends Repository {
2751
3295
  status: "planned"
2752
3296
  };
2753
3297
  const created = await this.create(draft, ctx);
2754
- await this.events.publish(createEvent(CRM_EVENTS.ACTIVITY_PLANNED, {
3298
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ACTIVITY_PLANNED, {
2755
3299
  activityId: String(created._id),
2756
3300
  type: created.type,
2757
3301
  subjectKind: created.subjectKind,
@@ -2765,7 +3309,7 @@ var ActivityRepository = class extends Repository {
2765
3309
  async complete(id, completedAt, ctx) {
2766
3310
  const ts = completedAt ?? /* @__PURE__ */ new Date();
2767
3311
  const updated = await this._transition(id, "completed", { completedAt: ts }, ctx);
2768
- await this.events.publish(createEvent(CRM_EVENTS.ACTIVITY_COMPLETED, {
3312
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ACTIVITY_COMPLETED, {
2769
3313
  activityId: id,
2770
3314
  completedAt: ts.toISOString()
2771
3315
  }, ctx));
@@ -2773,7 +3317,7 @@ var ActivityRepository = class extends Repository {
2773
3317
  }
2774
3318
  async cancel(id, reason, ctx) {
2775
3319
  const updated = await this._transition(id, "cancelled", reason ? { metadata: { cancelReason: reason } } : {}, ctx);
2776
- await this.events.publish(createEvent(CRM_EVENTS.ACTIVITY_CANCELLED, {
3320
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ACTIVITY_CANCELLED, {
2777
3321
  activityId: id,
2778
3322
  ...reason !== void 0 ? { reason } : {}
2779
3323
  }, ctx));
@@ -2795,7 +3339,7 @@ var ActivityRepository = class extends Repository {
2795
3339
  const previous = existing.dueDate;
2796
3340
  const updated = await this.update(id, { dueDate: newDueDate }, ctx);
2797
3341
  if (!updated) throw new NotFoundError("Activity", id);
2798
- await this.events.publish(createEvent(CRM_EVENTS.ACTIVITY_RESCHEDULED, {
3342
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ACTIVITY_RESCHEDULED, {
2799
3343
  activityId: id,
2800
3344
  dueDate: newDueDate.toISOString(),
2801
3345
  ...previous ? { previousDueDate: previous.toISOString() } : {},
@@ -2822,7 +3366,7 @@ var ActivityRepository = class extends Repository {
2822
3366
  };
2823
3367
  const updated = await this.update(id, { metadata: mergedMetadata }, ctx);
2824
3368
  if (!updated) throw new NotFoundError("Activity", id);
2825
- await this.events.publish(createEvent(CRM_EVENTS.ACTIVITY_OUTCOME_RECORDED, {
3369
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ACTIVITY_OUTCOME_RECORDED, {
2826
3370
  activityId: id,
2827
3371
  outcome
2828
3372
  }, ctx));
@@ -2879,7 +3423,7 @@ var NoteRepository = class extends Repository {
2879
3423
  }
2880
3424
  async addNote(input, ctx) {
2881
3425
  const created = await this.create(input, ctx);
2882
- await this.events.publish(createEvent(CRM_EVENTS.NOTE_ADDED, {
3426
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.NOTE_ADDED, {
2883
3427
  noteId: String(created._id),
2884
3428
  subjectKind: created.subjectKind,
2885
3429
  subjectId: String(created.subjectId)
@@ -2945,7 +3489,7 @@ var EmailEventRepository = class extends Repository {
2945
3489
  ...input.metadata !== void 0 ? { metadata: input.metadata } : {}
2946
3490
  };
2947
3491
  const created = await this.create(draft, ctx);
2948
- await this.events.publish(createEvent(KIND_TO_EVENT[kind], {
3492
+ await this.events.dispatch(ctx, createEvent(KIND_TO_EVENT[kind], {
2949
3493
  eventId: String(created._id),
2950
3494
  kind,
2951
3495
  subjectKind: input.subjectKind,
@@ -2975,7 +3519,7 @@ var ConversationRepository = class extends Repository {
2975
3519
  ...input.metadata !== void 0 ? { metadata: input.metadata } : {}
2976
3520
  };
2977
3521
  const created = await this.create(draft, ctx);
2978
- await this.events.publish(createEvent(CRM_EVENTS.CONVERSATION_CREATED, {
3522
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.CONVERSATION_CREATED, {
2979
3523
  conversationId: String(created._id),
2980
3524
  channel: created.channel,
2981
3525
  participantKind: created.participantKind,
@@ -2996,7 +3540,7 @@ var ConversationRepository = class extends Repository {
2996
3540
  if (!current) throw new NotFoundError("Conversation", id);
2997
3541
  return current;
2998
3542
  }
2999
- await this.events.publish(createEvent(CRM_EVENTS.CONVERSATION_ASSIGNED, {
3543
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.CONVERSATION_ASSIGNED, {
3000
3544
  conversationId: id,
3001
3545
  assigneeId,
3002
3546
  ...before.assigneeId !== void 0 ? { previousAssigneeId: before.assigneeId } : {}
@@ -3034,7 +3578,7 @@ var ConversationRepository = class extends Repository {
3034
3578
  if (existing.status === next) return existing;
3035
3579
  const updated = await this.update(id, { status: next }, ctx);
3036
3580
  if (!updated) throw new NotFoundError("Conversation", id);
3037
- await this.events.publish(createEvent(eventType, {
3581
+ await this.events.dispatch(ctx, createEvent(eventType, {
3038
3582
  conversationId: id,
3039
3583
  status: next,
3040
3584
  previousStatus: existing.status
@@ -3086,7 +3630,7 @@ var MessageRepository = class extends Repository {
3086
3630
  direction,
3087
3631
  sentAt
3088
3632
  }, ctx);
3089
- await this.events.publish(createEvent(eventType, {
3633
+ await this.events.dispatch(ctx, createEvent(eventType, {
3090
3634
  messageId: String(created._id),
3091
3635
  conversationId: input.conversationId,
3092
3636
  direction,
@@ -3111,6 +3655,230 @@ var MessageRepository = class extends Repository {
3111
3655
  }
3112
3656
  };
3113
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
3114
3882
  //#region src/repositories/create-repositories.ts
3115
3883
  /**
3116
3884
  * Repository factory — composes the shared mongokit plugin stack and
@@ -3144,6 +3912,7 @@ function createCrmRepositories(models, deps) {
3144
3912
  const account = new AccountRepository(models.Account, plugins, { events }, pagination);
3145
3913
  const campaign = new CampaignRepository(models.Campaign, plugins, { events }, pagination);
3146
3914
  const subscription = new SubscriptionRepository(models.Subscription, plugins, { events }, pagination);
3915
+ const suppression = new SuppressionRepository(models.Suppression, plugins, { events }, pagination);
3147
3916
  const activity = new ActivityRepository(models.Activity, plugins, { events }, pagination);
3148
3917
  const note = new NoteRepository(models.Note, plugins, { events }, pagination);
3149
3918
  const lossReason = new LossReasonRepository(models.LossReason, plugins, { events }, pagination);
@@ -3153,6 +3922,11 @@ function createCrmRepositories(models, deps) {
3153
3922
  events,
3154
3923
  conversations: conversation
3155
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);
3156
3930
  return Object.freeze({
3157
3931
  pipeline,
3158
3932
  lead,
@@ -3161,15 +3935,127 @@ function createCrmRepositories(models, deps) {
3161
3935
  account,
3162
3936
  campaign,
3163
3937
  subscription,
3938
+ suppression,
3164
3939
  activity,
3165
3940
  note,
3166
3941
  lossReason,
3167
3942
  emailEvent,
3168
3943
  conversation,
3169
- message
3944
+ message,
3945
+ sequence,
3946
+ enrollment
3170
3947
  });
3171
3948
  }
3172
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
4022
+ //#region src/events/in-process-bus.ts
4023
+ /**
4024
+ * In-process event bus — default fallback when no `eventTransport` is
4025
+ * provided to `createCrm`. Structurally identical to arc's
4026
+ * `MemoryEventTransport`; pattern matching is delegated to primitives.
4027
+ * Handler errors are swallowed — event emission never blocks writes.
4028
+ */
4029
+ var InProcessCrmBus = class {
4030
+ name = "in-process-crm";
4031
+ subs = [];
4032
+ logger;
4033
+ constructor(options) {
4034
+ if (options?.logger) this.logger = options.logger;
4035
+ }
4036
+ async publish(event) {
4037
+ const matching = this.subs.filter((s) => matchEventPattern(s.pattern, event.type));
4038
+ for (const sub of matching) try {
4039
+ await sub.handler(event);
4040
+ } catch (err) {
4041
+ this.logger?.error?.(`[crm] event handler error [${event.type}]`, { error: err instanceof Error ? err.message : String(err) });
4042
+ }
4043
+ }
4044
+ async subscribe(pattern, handler) {
4045
+ const sub = {
4046
+ pattern,
4047
+ handler
4048
+ };
4049
+ this.subs.push(sub);
4050
+ return () => {
4051
+ this.subs = this.subs.filter((s) => s !== sub);
4052
+ };
4053
+ }
4054
+ async close() {
4055
+ this.subs = [];
4056
+ }
4057
+ };
4058
+ //#endregion
3173
4059
  //#region src/engine/ensure-ready.ts
3174
4060
  const REQUIRED_CAPABILITIES = [
3175
4061
  "upsert",
@@ -3188,6 +4074,42 @@ function assertCrmCapabilities(probe, options = {}) {
3188
4074
  }
3189
4075
  }
3190
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
3191
4113
  //#region src/engine/create-crm.ts
3192
4114
  async function createCrm(config) {
3193
4115
  if (!config.connection) throw new Error("[crm] CrmConfig.connection is required");
@@ -3201,7 +4123,11 @@ async function createCrm(config) {
3201
4123
  ...config.pagination !== void 0 ? { pagination: config.pagination } : {},
3202
4124
  plugins: config.plugins ?? []
3203
4125
  };
3204
- 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
+ });
3205
4131
  const models = createCrmModels(config.connection, resolved);
3206
4132
  const repositories = createCrmRepositories(models, {
3207
4133
  events,
@@ -3216,11 +4142,19 @@ async function createCrm(config) {
3216
4142
  repositories,
3217
4143
  events,
3218
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
+ },
3219
4153
  async syncIndexes() {
3220
4154
  await Promise.all(Object.values(models).map((m) => m.syncIndexes()));
3221
4155
  },
3222
4156
  async dispose() {
3223
- await events.close?.();
4157
+ await events.close();
3224
4158
  }
3225
4159
  });
3226
4160
  }
@@ -3312,7 +4246,7 @@ var LeadService = class LeadService {
3312
4246
  opportunityId: String(opportunity._id),
3313
4247
  ...input.by !== void 0 ? { by: input.by } : {}
3314
4248
  }, ctx);
3315
- await this.events.publish(createEvent(CRM_EVENTS.LEAD_CONVERTED, {
4249
+ await this.events.dispatch(ctx, createEvent(CRM_EVENTS.LEAD_CONVERTED, {
3316
4250
  leadId,
3317
4251
  contactId: String(contact._id),
3318
4252
  accountId: account ? String(account._id) : void 0,
@@ -3400,14 +4334,721 @@ function splitLeadName(lead) {
3400
4334
  };
3401
4335
  }
3402
4336
  //#endregion
4337
+ //#region src/events/event-catalog.ts
4338
+ /**
4339
+ * Zod-source event catalog (PACKAGE_RULES rule 18).
4340
+ *
4341
+ * One Zod schema per event is the source of truth — runtime validation,
4342
+ * static types, and Arc introspection all derive from it. Hosts wire the
4343
+ * definitions into arc's `EventRegistry`:
4344
+ *
4345
+ * ```ts
4346
+ * for (const def of crmEventDefinitions) registry.register(def);
4347
+ * ```
4348
+ *
4349
+ * which lights up publish-time validation (`eventPlugin({ registry,
4350
+ * validateMode: 'reject' })`), OpenAPI event introspection, and
4351
+ * `arc describe` output — with zero arc imports here (the definition shape
4352
+ * is structurally identical to arc's `EventDefinitionOutput`).
4353
+ *
4354
+ * Timestamps in payloads are `z.iso.datetime()` (NEVER `z.coerce.date()`):
4355
+ * `z.toJSONSchema()` throws on coerce.date, and events are ISO strings on
4356
+ * the wire (rule 25).
4357
+ */
4358
+ function defineCrmEvent(input) {
4359
+ const { name, resource, description, version = 1, zodSchema } = input;
4360
+ return {
4361
+ name,
4362
+ version,
4363
+ description,
4364
+ zodSchema,
4365
+ schema: z.toJSONSchema(zodSchema),
4366
+ create(payload, meta) {
4367
+ return createEvent$1(name, payload, {
4368
+ resource,
4369
+ source: "crm",
4370
+ ...meta
4371
+ });
4372
+ }
4373
+ };
4374
+ }
4375
+ const ownerChangeSchema = z.object({
4376
+ ownerId: z.string(),
4377
+ previousOwnerId: z.string().optional()
4378
+ });
4379
+ const enrichSchema = z.object({
4380
+ fields: z.array(z.string()),
4381
+ provider: z.string().optional()
4382
+ });
4383
+ const mergeSchema = z.object({
4384
+ primaryId: z.string(),
4385
+ secondaryId: z.string(),
4386
+ strategy: z.enum(["preferPrimary", "preferSecondary"])
4387
+ });
4388
+ const stageRefSchema = z.object({
4389
+ stageId: z.string(),
4390
+ sequence: z.number()
4391
+ });
4392
+ const LeadCreated = defineCrmEvent({
4393
+ name: CRM_EVENTS.LEAD_CREATED,
4394
+ resource: "lead",
4395
+ description: "createLead() inserted a scored lead.",
4396
+ zodSchema: z.object({
4397
+ leadId: z.string(),
4398
+ email: z.string().optional(),
4399
+ score: z.number()
4400
+ })
4401
+ });
4402
+ const LeadStatusChanged = defineCrmEvent({
4403
+ name: CRM_EVENTS.LEAD_STATUS_CHANGED,
4404
+ resource: "lead",
4405
+ description: "A lead lifecycle transition committed (qualify/disqualify/nurture/...).",
4406
+ zodSchema: z.object({
4407
+ leadId: z.string(),
4408
+ from: z.string(),
4409
+ to: z.string()
4410
+ })
4411
+ });
4412
+ const LeadScored = defineCrmEvent({
4413
+ name: CRM_EVENTS.LEAD_SCORED,
4414
+ resource: "lead",
4415
+ description: "A lead score was (re)computed and persisted.",
4416
+ zodSchema: z.object({
4417
+ leadId: z.string(),
4418
+ score: z.number()
4419
+ })
4420
+ });
4421
+ const LeadConverted = defineCrmEvent({
4422
+ name: CRM_EVENTS.LEAD_CONVERTED,
4423
+ resource: "lead",
4424
+ description: "convert() spawned Contact (+ Account) + Opportunity and closed the lead.",
4425
+ zodSchema: z.object({
4426
+ leadId: z.string(),
4427
+ contactId: z.string(),
4428
+ accountId: z.string().optional(),
4429
+ opportunityId: z.string()
4430
+ })
4431
+ });
4432
+ const LeadDisqualified = defineCrmEvent({
4433
+ name: CRM_EVENTS.LEAD_DISQUALIFIED,
4434
+ resource: "lead",
4435
+ description: "disqualify() rejected a lead with a reason.",
4436
+ zodSchema: z.object({
4437
+ leadId: z.string(),
4438
+ reason: z.string()
4439
+ })
4440
+ });
4441
+ const LeadEnriched = defineCrmEvent({
4442
+ name: CRM_EVENTS.LEAD_ENRICHED,
4443
+ resource: "lead",
4444
+ description: "enrich() merged a provider/manual patch into the lead.",
4445
+ zodSchema: z.object({ leadId: z.string() }).extend(enrichSchema.shape)
4446
+ });
4447
+ const LeadAssigned = defineCrmEvent({
4448
+ name: CRM_EVENTS.LEAD_ASSIGNED,
4449
+ resource: "lead",
4450
+ description: "assign() (re)assigned the lead owner.",
4451
+ zodSchema: z.object({ leadId: z.string() }).extend(ownerChangeSchema.shape)
4452
+ });
4453
+ const AccountCreated = defineCrmEvent({
4454
+ name: CRM_EVENTS.ACCOUNT_CREATED,
4455
+ resource: "account",
4456
+ description: "createAccount() inserted an account.",
4457
+ zodSchema: z.object({
4458
+ accountId: z.string(),
4459
+ name: z.string()
4460
+ })
4461
+ });
4462
+ const AccountUpdated = defineCrmEvent({
4463
+ name: CRM_EVENTS.ACCOUNT_UPDATED,
4464
+ resource: "account",
4465
+ description: "An account record was updated.",
4466
+ zodSchema: z.object({ accountId: z.string() })
4467
+ });
4468
+ const AccountEnriched = defineCrmEvent({
4469
+ name: CRM_EVENTS.ACCOUNT_ENRICHED,
4470
+ resource: "account",
4471
+ description: "enrich() merged a patch into the account.",
4472
+ zodSchema: z.object({ accountId: z.string() }).extend(enrichSchema.shape)
4473
+ });
4474
+ const AccountAssigned = defineCrmEvent({
4475
+ name: CRM_EVENTS.ACCOUNT_ASSIGNED,
4476
+ resource: "account",
4477
+ description: "assign() (re)assigned the account owner.",
4478
+ zodSchema: z.object({ accountId: z.string() }).extend(ownerChangeSchema.shape)
4479
+ });
4480
+ const AccountMerged = defineCrmEvent({
4481
+ name: CRM_EVENTS.ACCOUNT_MERGED,
4482
+ resource: "account",
4483
+ description: "merge() folded a secondary account into the primary; secondary deleted.",
4484
+ zodSchema: mergeSchema
4485
+ });
4486
+ const ContactCreated = defineCrmEvent({
4487
+ name: CRM_EVENTS.CONTACT_CREATED,
4488
+ resource: "contact",
4489
+ description: "createContact() inserted a contact.",
4490
+ zodSchema: z.object({
4491
+ contactId: z.string(),
4492
+ email: z.string().optional()
4493
+ })
4494
+ });
4495
+ const ContactUpdated = defineCrmEvent({
4496
+ name: CRM_EVENTS.CONTACT_UPDATED,
4497
+ resource: "contact",
4498
+ description: "A contact record was updated.",
4499
+ zodSchema: z.object({ contactId: z.string() })
4500
+ });
4501
+ const ContactEnriched = defineCrmEvent({
4502
+ name: CRM_EVENTS.CONTACT_ENRICHED,
4503
+ resource: "contact",
4504
+ description: "enrich() merged a patch into the contact.",
4505
+ zodSchema: z.object({ contactId: z.string() }).extend(enrichSchema.shape)
4506
+ });
4507
+ const ContactAssigned = defineCrmEvent({
4508
+ name: CRM_EVENTS.CONTACT_ASSIGNED,
4509
+ resource: "contact",
4510
+ description: "assign() (re)assigned the contact owner.",
4511
+ zodSchema: z.object({ contactId: z.string() }).extend(ownerChangeSchema.shape)
4512
+ });
4513
+ const ContactMerged = defineCrmEvent({
4514
+ name: CRM_EVENTS.CONTACT_MERGED,
4515
+ resource: "contact",
4516
+ description: "merge() folded a secondary contact into the primary; secondary deleted.",
4517
+ zodSchema: mergeSchema
4518
+ });
4519
+ const OpportunityCreated = defineCrmEvent({
4520
+ name: CRM_EVENTS.OPPORTUNITY_CREATED,
4521
+ resource: "opportunity",
4522
+ description: "openOpportunity() created a deal in a pipeline stage.",
4523
+ zodSchema: z.object({
4524
+ opportunityId: z.string(),
4525
+ pipelineId: z.string(),
4526
+ stageId: z.string()
4527
+ })
4528
+ });
4529
+ const OpportunityStageAdvanced = defineCrmEvent({
4530
+ name: CRM_EVENTS.OPPORTUNITY_STAGE_ADVANCED,
4531
+ resource: "opportunity",
4532
+ description: "moveToStage() moved the deal forward (higher stage sequence).",
4533
+ zodSchema: z.object({
4534
+ opportunityId: z.string(),
4535
+ from: stageRefSchema,
4536
+ to: stageRefSchema
4537
+ })
4538
+ });
4539
+ const OpportunityStageRegressed = defineCrmEvent({
4540
+ name: CRM_EVENTS.OPPORTUNITY_STAGE_REGRESSED,
4541
+ resource: "opportunity",
4542
+ description: "moveToStage() moved the deal backward (lower stage sequence).",
4543
+ zodSchema: z.object({
4544
+ opportunityId: z.string(),
4545
+ from: stageRefSchema,
4546
+ to: stageRefSchema
4547
+ })
4548
+ });
4549
+ const OpportunityWon = defineCrmEvent({
4550
+ name: CRM_EVENTS.OPPORTUNITY_WON,
4551
+ resource: "opportunity",
4552
+ description: "win() closed the deal as won (terminal).",
4553
+ zodSchema: z.object({
4554
+ opportunityId: z.string(),
4555
+ amount: z.number().optional()
4556
+ })
4557
+ });
4558
+ const OpportunityLost = defineCrmEvent({
4559
+ name: CRM_EVENTS.OPPORTUNITY_LOST,
4560
+ resource: "opportunity",
4561
+ description: "lose() closed the deal as lost (terminal).",
4562
+ zodSchema: z.object({
4563
+ opportunityId: z.string(),
4564
+ lostReasonId: z.string().optional()
4565
+ })
4566
+ });
4567
+ const OpportunityAbandoned = defineCrmEvent({
4568
+ name: CRM_EVENTS.OPPORTUNITY_ABANDONED,
4569
+ resource: "opportunity",
4570
+ description: "abandon() closed the deal as abandoned (terminal).",
4571
+ zodSchema: z.object({ opportunityId: z.string() })
4572
+ });
4573
+ const OpportunityAssigned = defineCrmEvent({
4574
+ name: CRM_EVENTS.OPPORTUNITY_ASSIGNED,
4575
+ resource: "opportunity",
4576
+ description: "assign() (re)assigned the opportunity owner.",
4577
+ zodSchema: z.object({ opportunityId: z.string() }).extend(ownerChangeSchema.shape)
4578
+ });
4579
+ const OpportunityAmountChanged = defineCrmEvent({
4580
+ name: CRM_EVENTS.OPPORTUNITY_AMOUNT_CHANGED,
4581
+ resource: "opportunity",
4582
+ description: "updateAmount() changed an open deal value.",
4583
+ zodSchema: z.object({
4584
+ opportunityId: z.string(),
4585
+ amount: z.number(),
4586
+ previousAmount: z.number().optional(),
4587
+ reason: z.string().optional(),
4588
+ by: z.string().optional()
4589
+ })
4590
+ });
4591
+ const OpportunityExtended = defineCrmEvent({
4592
+ name: CRM_EVENTS.OPPORTUNITY_EXTENDED,
4593
+ resource: "opportunity",
4594
+ description: "extendCloseDate() pushed expectedCloseAt further out.",
4595
+ zodSchema: z.object({
4596
+ opportunityId: z.string(),
4597
+ expectedCloseAt: z.iso.datetime(),
4598
+ previousExpectedCloseAt: z.iso.datetime().optional(),
4599
+ reason: z.string().optional(),
4600
+ by: z.string().optional()
4601
+ })
4602
+ });
4603
+ const PipelineCreated = defineCrmEvent({
4604
+ name: CRM_EVENTS.PIPELINE_CREATED,
4605
+ resource: "pipeline",
4606
+ description: "A pipeline was created.",
4607
+ zodSchema: z.object({ pipelineId: z.string() })
4608
+ });
4609
+ const PipelineArchived = defineCrmEvent({
4610
+ name: CRM_EVENTS.PIPELINE_ARCHIVED,
4611
+ resource: "pipeline",
4612
+ description: "archive() soft-deprecated a pipeline.",
4613
+ zodSchema: z.object({ pipelineId: z.string() })
4614
+ });
4615
+ const ActivityPlanned = defineCrmEvent({
4616
+ name: CRM_EVENTS.ACTIVITY_PLANNED,
4617
+ resource: "activity",
4618
+ description: "plan() created a planned activity against a subject.",
4619
+ zodSchema: z.object({
4620
+ activityId: z.string(),
4621
+ type: z.string(),
4622
+ subjectKind: z.string(),
4623
+ subjectId: z.string()
4624
+ })
4625
+ });
4626
+ const ActivityCompleted = defineCrmEvent({
4627
+ name: CRM_EVENTS.ACTIVITY_COMPLETED,
4628
+ resource: "activity",
4629
+ description: "complete() finished an activity.",
4630
+ zodSchema: z.object({
4631
+ activityId: z.string(),
4632
+ completedAt: z.iso.datetime()
4633
+ })
4634
+ });
4635
+ const ActivityCancelled = defineCrmEvent({
4636
+ name: CRM_EVENTS.ACTIVITY_CANCELLED,
4637
+ resource: "activity",
4638
+ description: "cancel() cancelled an activity.",
4639
+ zodSchema: z.object({
4640
+ activityId: z.string(),
4641
+ reason: z.string().optional()
4642
+ })
4643
+ });
4644
+ const ActivityRescheduled = defineCrmEvent({
4645
+ name: CRM_EVENTS.ACTIVITY_RESCHEDULED,
4646
+ resource: "activity",
4647
+ description: "reschedule() moved an activity due date.",
4648
+ zodSchema: z.object({
4649
+ activityId: z.string(),
4650
+ dueDate: z.iso.datetime(),
4651
+ previousDueDate: z.iso.datetime().optional(),
4652
+ reason: z.string().optional(),
4653
+ by: z.string().optional()
4654
+ })
4655
+ });
4656
+ const ActivityOutcomeRecorded = defineCrmEvent({
4657
+ name: CRM_EVENTS.ACTIVITY_OUTCOME_RECORDED,
4658
+ resource: "activity",
4659
+ description: "recordOutcome() logged an activity outcome (no status change).",
4660
+ zodSchema: z.object({
4661
+ activityId: z.string(),
4662
+ outcome: z.record(z.string(), z.unknown())
4663
+ })
4664
+ });
4665
+ const NoteAdded = defineCrmEvent({
4666
+ name: CRM_EVENTS.NOTE_ADDED,
4667
+ resource: "note",
4668
+ description: "A note was appended to a subject timeline.",
4669
+ zodSchema: z.object({
4670
+ noteId: z.string(),
4671
+ subjectKind: z.string(),
4672
+ subjectId: z.string()
4673
+ })
4674
+ });
4675
+ const CampaignCreated = defineCrmEvent({
4676
+ name: CRM_EVENTS.CAMPAIGN_CREATED,
4677
+ resource: "campaign",
4678
+ description: "createCampaign() inserted a campaign.",
4679
+ zodSchema: z.object({
4680
+ campaignId: z.string(),
4681
+ name: z.string()
4682
+ })
4683
+ });
4684
+ const CampaignActivated = defineCrmEvent({
4685
+ name: CRM_EVENTS.CAMPAIGN_ACTIVATED,
4686
+ resource: "campaign",
4687
+ description: "activate() moved a campaign to active.",
4688
+ zodSchema: z.object({ campaignId: z.string() })
4689
+ });
4690
+ const CampaignPaused = defineCrmEvent({
4691
+ name: CRM_EVENTS.CAMPAIGN_PAUSED,
4692
+ resource: "campaign",
4693
+ description: "pause() suspended an active campaign.",
4694
+ zodSchema: z.object({
4695
+ campaignId: z.string(),
4696
+ reason: z.string().optional()
4697
+ })
4698
+ });
4699
+ const CampaignResumed = defineCrmEvent({
4700
+ name: CRM_EVENTS.CAMPAIGN_RESUMED,
4701
+ resource: "campaign",
4702
+ description: "resume() reactivated a paused campaign.",
4703
+ zodSchema: z.object({ campaignId: z.string() })
4704
+ });
4705
+ const CampaignCompleted = defineCrmEvent({
4706
+ name: CRM_EVENTS.CAMPAIGN_COMPLETED,
4707
+ resource: "campaign",
4708
+ description: "complete() closed a campaign.",
4709
+ zodSchema: z.object({
4710
+ campaignId: z.string(),
4711
+ actualRevenueAmount: z.number().optional()
4712
+ })
4713
+ });
4714
+ const CampaignArchived = defineCrmEvent({
4715
+ name: CRM_EVENTS.CAMPAIGN_ARCHIVED,
4716
+ resource: "campaign",
4717
+ description: "archive() archived a campaign (terminal).",
4718
+ zodSchema: z.object({ campaignId: z.string() })
4719
+ });
4720
+ const SubscriptionCreated = defineCrmEvent({
4721
+ name: CRM_EVENTS.SUBSCRIPTION_CREATED,
4722
+ resource: "subscription",
4723
+ description: "subscribe() created an opt-in (pending or confirmed).",
4724
+ zodSchema: z.object({
4725
+ subscriptionId: z.string(),
4726
+ contactId: z.string(),
4727
+ campaignId: z.string(),
4728
+ requiresConfirmation: z.boolean()
4729
+ })
4730
+ });
4731
+ const SubscriptionConfirmed = defineCrmEvent({
4732
+ name: CRM_EVENTS.SUBSCRIPTION_CONFIRMED,
4733
+ resource: "subscription",
4734
+ description: "confirm() validated a double-opt-in token.",
4735
+ zodSchema: z.object({
4736
+ subscriptionId: z.string(),
4737
+ contactId: z.string(),
4738
+ campaignId: z.string(),
4739
+ confirmedAt: z.iso.datetime()
4740
+ })
4741
+ });
4742
+ const SubscriptionUnsubscribed = defineCrmEvent({
4743
+ name: CRM_EVENTS.SUBSCRIPTION_UNSUBSCRIBED,
4744
+ resource: "subscription",
4745
+ description: "unsubscribe() opted a contact out (terminal).",
4746
+ zodSchema: z.object({
4747
+ subscriptionId: z.string(),
4748
+ contactId: z.string(),
4749
+ campaignId: z.string(),
4750
+ reason: z.string().optional()
4751
+ })
4752
+ });
4753
+ const SubscriptionBounced = defineCrmEvent({
4754
+ name: CRM_EVENTS.SUBSCRIPTION_BOUNCED,
4755
+ resource: "subscription",
4756
+ description: "markBounced() recorded a hard bounce (terminal).",
4757
+ zodSchema: z.object({
4758
+ subscriptionId: z.string(),
4759
+ contactId: z.string(),
4760
+ campaignId: z.string(),
4761
+ reason: z.string().optional()
4762
+ })
4763
+ });
4764
+ const SubscriptionComplained = defineCrmEvent({
4765
+ name: CRM_EVENTS.SUBSCRIPTION_COMPLAINED,
4766
+ resource: "subscription",
4767
+ description: "markComplained() recorded a spam complaint (terminal).",
4768
+ zodSchema: z.object({
4769
+ subscriptionId: z.string(),
4770
+ contactId: z.string(),
4771
+ campaignId: z.string()
4772
+ })
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
+ });
4790
+ const emailEventSchema = z.object({
4791
+ eventId: z.string(),
4792
+ kind: z.string(),
4793
+ subjectKind: z.string(),
4794
+ subjectId: z.string(),
4795
+ campaignId: z.string().optional()
4796
+ });
4797
+ const EmailEventOpened = defineCrmEvent({
4798
+ name: CRM_EVENTS.EMAIL_EVENT_OPENED,
4799
+ resource: "email_event",
4800
+ description: "recordOpen() logged an email open.",
4801
+ zodSchema: emailEventSchema
4802
+ });
4803
+ const EmailEventClicked = defineCrmEvent({
4804
+ name: CRM_EVENTS.EMAIL_EVENT_CLICKED,
4805
+ resource: "email_event",
4806
+ description: "recordClick() logged a link click.",
4807
+ zodSchema: emailEventSchema
4808
+ });
4809
+ const EmailEventBounced = defineCrmEvent({
4810
+ name: CRM_EVENTS.EMAIL_EVENT_BOUNCED,
4811
+ resource: "email_event",
4812
+ description: "recordBounce() logged a bounce.",
4813
+ zodSchema: emailEventSchema
4814
+ });
4815
+ const EmailEventComplained = defineCrmEvent({
4816
+ name: CRM_EVENTS.EMAIL_EVENT_COMPLAINED,
4817
+ resource: "email_event",
4818
+ description: "recordComplaint() logged a spam complaint.",
4819
+ zodSchema: emailEventSchema
4820
+ });
4821
+ const EmailEventUnsubscribed = defineCrmEvent({
4822
+ name: CRM_EVENTS.EMAIL_EVENT_UNSUBSCRIBED,
4823
+ resource: "email_event",
4824
+ description: "recordUnsubscribe() logged a one-click unsubscribe.",
4825
+ zodSchema: emailEventSchema
4826
+ });
4827
+ const ConversationCreated = defineCrmEvent({
4828
+ name: CRM_EVENTS.CONVERSATION_CREATED,
4829
+ resource: "conversation",
4830
+ description: "openConversation() started a conversation thread.",
4831
+ zodSchema: z.object({
4832
+ conversationId: z.string(),
4833
+ channel: z.string(),
4834
+ participantKind: z.string(),
4835
+ participantId: z.string()
4836
+ })
4837
+ });
4838
+ const ConversationAssigned = defineCrmEvent({
4839
+ name: CRM_EVENTS.CONVERSATION_ASSIGNED,
4840
+ resource: "conversation",
4841
+ description: "assign() (re)assigned a conversation owner.",
4842
+ zodSchema: z.object({
4843
+ conversationId: z.string(),
4844
+ assigneeId: z.string(),
4845
+ previousAssigneeId: z.string().optional()
4846
+ })
4847
+ });
4848
+ const ConversationResolved = defineCrmEvent({
4849
+ name: CRM_EVENTS.CONVERSATION_RESOLVED,
4850
+ resource: "conversation",
4851
+ description: "resolve() closed a conversation.",
4852
+ zodSchema: z.object({
4853
+ conversationId: z.string(),
4854
+ status: z.string(),
4855
+ previousStatus: z.string()
4856
+ })
4857
+ });
4858
+ const ConversationReopened = defineCrmEvent({
4859
+ name: CRM_EVENTS.CONVERSATION_REOPENED,
4860
+ resource: "conversation",
4861
+ description: "reopen() reopened a resolved conversation.",
4862
+ zodSchema: z.object({
4863
+ conversationId: z.string(),
4864
+ status: z.string(),
4865
+ previousStatus: z.string()
4866
+ })
4867
+ });
4868
+ const MessageReceived = defineCrmEvent({
4869
+ name: CRM_EVENTS.MESSAGE_RECEIVED,
4870
+ resource: "message",
4871
+ description: "recordInbound() logged an inbound message.",
4872
+ zodSchema: z.object({
4873
+ messageId: z.string(),
4874
+ conversationId: z.string(),
4875
+ direction: z.string(),
4876
+ externalMessageId: z.string().optional()
4877
+ })
4878
+ });
4879
+ const MessageSent = defineCrmEvent({
4880
+ name: CRM_EVENTS.MESSAGE_SENT,
4881
+ resource: "message",
4882
+ description: "recordOutbound() logged an outbound message.",
4883
+ zodSchema: z.object({
4884
+ messageId: z.string(),
4885
+ conversationId: z.string(),
4886
+ direction: z.string(),
4887
+ externalMessageId: z.string().optional()
4888
+ })
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
+ });
4966
+ const crmEventDefinitions = [
4967
+ LeadCreated,
4968
+ LeadStatusChanged,
4969
+ LeadScored,
4970
+ LeadConverted,
4971
+ LeadDisqualified,
4972
+ LeadEnriched,
4973
+ LeadAssigned,
4974
+ AccountCreated,
4975
+ AccountUpdated,
4976
+ AccountEnriched,
4977
+ AccountAssigned,
4978
+ AccountMerged,
4979
+ ContactCreated,
4980
+ ContactUpdated,
4981
+ ContactEnriched,
4982
+ ContactAssigned,
4983
+ ContactMerged,
4984
+ OpportunityCreated,
4985
+ OpportunityStageAdvanced,
4986
+ OpportunityStageRegressed,
4987
+ OpportunityWon,
4988
+ OpportunityLost,
4989
+ OpportunityAbandoned,
4990
+ OpportunityAssigned,
4991
+ OpportunityAmountChanged,
4992
+ OpportunityExtended,
4993
+ PipelineCreated,
4994
+ PipelineArchived,
4995
+ ActivityPlanned,
4996
+ ActivityCompleted,
4997
+ ActivityCancelled,
4998
+ ActivityRescheduled,
4999
+ ActivityOutcomeRecorded,
5000
+ NoteAdded,
5001
+ CampaignCreated,
5002
+ CampaignActivated,
5003
+ CampaignPaused,
5004
+ CampaignResumed,
5005
+ CampaignCompleted,
5006
+ CampaignArchived,
5007
+ SubscriptionCreated,
5008
+ SubscriptionConfirmed,
5009
+ SubscriptionUnsubscribed,
5010
+ SubscriptionBounced,
5011
+ SubscriptionComplained,
5012
+ SuppressionAdded,
5013
+ SuppressionRemoved,
5014
+ EmailEventOpened,
5015
+ EmailEventClicked,
5016
+ EmailEventBounced,
5017
+ EmailEventComplained,
5018
+ EmailEventUnsubscribed,
5019
+ ConversationCreated,
5020
+ ConversationAssigned,
5021
+ ConversationResolved,
5022
+ ConversationReopened,
5023
+ MessageReceived,
5024
+ MessageSent,
5025
+ SequenceCreated,
5026
+ SequenceActivated,
5027
+ SequencePaused,
5028
+ SequenceArchived,
5029
+ EnrollmentCreated,
5030
+ EnrollmentStepSent,
5031
+ EnrollmentPaused,
5032
+ EnrollmentResumed,
5033
+ EnrollmentCompleted,
5034
+ EnrollmentExited
5035
+ ];
5036
+ //#endregion
3403
5037
  //#region src/shared/calculators/forecast.ts
3404
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
+ /**
3405
5046
  * Aggregate opportunities into a forecast table — one row per bucket.
3406
5047
  * Currency mismatch within a bucket throws — partition by currency
3407
5048
  * upstream when multi-currency.
3408
5049
  */
3409
5050
  function forecast(opportunities, options = {}) {
3410
- const bucketFn = options.bucket ?? defaultBucket;
5051
+ const bucketFn = options.bucket ?? (options.timeZone !== void 0 ? zoneBucket(options.timeZone) : defaultBucket);
3411
5052
  const include = options.include ?? defaultInclude;
3412
5053
  const rows = /* @__PURE__ */ new Map();
3413
5054
  for (const o of opportunities) {
@@ -3440,6 +5081,10 @@ function defaultBucket(o) {
3440
5081
  const d = o.expectedCloseAt;
3441
5082
  return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}`;
3442
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
+ }
3443
5088
  /**
3444
5089
  * "Where do deals die?" summary — opportunities partitioned across
3445
5090
  * stages with current count + won/lost-from-here.
@@ -3476,7 +5121,7 @@ const objectIdLike = z.union([z.string().min(1), z.string()]);
3476
5121
  const metadataSchema = z.record(z.string(), z.unknown()).optional();
3477
5122
  const tagsSchema = z.array(z.string().min(1).max(64)).max(50).optional();
3478
5123
  const moneyAmountSchema = z.number().int().min(0).max(Number.MAX_SAFE_INTEGER);
3479
- 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)");
3480
5125
  //#endregion
3481
5126
  //#region src/validators/pipeline.schema.ts
3482
5127
  const stageInputSchema = z.object({
@@ -3530,7 +5175,7 @@ const convertLeadSchema = z.object({
3530
5175
  pipelineId: objectIdLike,
3531
5176
  opportunityName: z.string().min(1).max(200).optional(),
3532
5177
  amount: z.number().nonnegative().optional(),
3533
- currency: z.string().regex(/^[A-Z]{3}$/).optional(),
5178
+ currency: currencyCodeSchema.optional(),
3534
5179
  expectedCloseAt: z.coerce.date().optional(),
3535
5180
  by: z.string().optional()
3536
5181
  });
@@ -3686,6 +5331,21 @@ const subscriptionFilterSchema = z.object({
3686
5331
  campaignId: objectIdLike.optional()
3687
5332
  });
3688
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
3689
5349
  //#region src/validators/activity.schema.ts
3690
5350
  const planActivitySchema = z.object({
3691
5351
  type: z.enum(ACTIVITY_TYPES),
@@ -3762,6 +5422,54 @@ const emailEventFilterSchema = z.object({
3762
5422
  messageId: z.string().optional()
3763
5423
  });
3764
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
3765
5473
  //#region src/validators/index.ts
3766
5474
  var validators_exports = /* @__PURE__ */ __exportAll({
3767
5475
  accountFilterSchema: () => accountFilterSchema,
@@ -3781,6 +5489,7 @@ var validators_exports = /* @__PURE__ */ __exportAll({
3781
5489
  createLeadSchema: () => createLeadSchema,
3782
5490
  createLossReasonSchema: () => createLossReasonSchema,
3783
5491
  createPipelineSchema: () => createPipelineSchema,
5492
+ createSequenceSchema: () => createSequenceSchema,
3784
5493
  currencyCodeSchema: () => currencyCodeSchema,
3785
5494
  emailEventFilterSchema: () => emailEventFilterSchema,
3786
5495
  leadFilterSchema: () => leadFilterSchema,
@@ -3795,11 +5504,16 @@ var validators_exports = /* @__PURE__ */ __exportAll({
3795
5504
  pipelineFilterSchema: () => pipelineFilterSchema,
3796
5505
  planActivitySchema: () => planActivitySchema,
3797
5506
  recordEmailEventSchema: () => recordEmailEventSchema,
5507
+ sequenceFilterSchema: () => sequenceFilterSchema,
5508
+ sequenceStepSchema: () => sequenceStepSchema,
3798
5509
  stageInputSchemaExport: () => stageInputSchemaExport,
3799
5510
  subscribeSchema: () => subscribeSchema,
3800
5511
  subscriptionFilterSchema: () => subscriptionFilterSchema,
5512
+ suppressSchema: () => suppressSchema,
5513
+ suppressionFilterSchema: () => suppressionFilterSchema,
3801
5514
  tagsSchema: () => tagsSchema,
3802
5515
  unsubscribeSchema: () => unsubscribeSchema,
5516
+ unsuppressSchema: () => unsuppressSchema,
3803
5517
  updateAccountSchema: () => updateAccountSchema,
3804
5518
  updateActivitySchema: () => updateActivitySchema,
3805
5519
  updateCampaignSchema: () => updateCampaignSchema,
@@ -3807,7 +5521,8 @@ var validators_exports = /* @__PURE__ */ __exportAll({
3807
5521
  updateLeadSchema: () => updateLeadSchema,
3808
5522
  updateLossReasonSchema: () => updateLossReasonSchema,
3809
5523
  updateNoteSchema: () => updateNoteSchema,
3810
- updatePipelineSchema: () => updatePipelineSchema
5524
+ updatePipelineSchema: () => updatePipelineSchema,
5525
+ updateSequenceSchema: () => updateSequenceSchema
3811
5526
  });
3812
5527
  //#endregion
3813
- 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 };