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