@classytic/crm 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +124 -0
- package/dist/events.d.ts +475 -2
- package/dist/events.js +180 -59
- package/dist/index.d.ts +2095 -780
- package/dist/index.js +1787 -72
- package/package.json +8 -8
- package/dist/index2.d.ts +0 -746
- /package/dist/{rolldown-runtime.js → chunk.js} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { t as __exportAll } from "./
|
|
2
|
-
import { AccountAssigned, AccountCreated, AccountEnriched, AccountMerged, AccountUpdated, ActivityCancelled, ActivityCompleted, ActivityOutcomeRecorded, ActivityPlanned, ActivityRescheduled, CRM_EVENTS, CampaignActivated, CampaignArchived, CampaignCompleted, CampaignCreated, CampaignPaused, CampaignResumed, ContactAssigned, ContactCreated, ContactEnriched, ContactMerged, ContactUpdated, ConversationAssigned, ConversationCreated, ConversationReopened, ConversationResolved, EmailEventBounced, EmailEventClicked, EmailEventComplained, EmailEventOpened, EmailEventUnsubscribed, InProcessCrmBus, LeadAssigned, LeadConverted, LeadCreated, LeadDisqualified, LeadEnriched, LeadScored, LeadStatusChanged, MessageReceived, MessageSent, NoteAdded, OpportunityAbandoned, OpportunityAmountChanged, OpportunityAssigned, OpportunityCreated, OpportunityExtended, OpportunityLost, OpportunityStageAdvanced, OpportunityStageRegressed, OpportunityWon, PipelineArchived, PipelineCreated, SubscriptionBounced, SubscriptionComplained, SubscriptionConfirmed, SubscriptionCreated, SubscriptionUnsubscribed, createEvent, crmEventDefinitions } from "./events.js";
|
|
1
|
+
import { t as __exportAll } from "./chunk.js";
|
|
3
2
|
import { Schema } from "mongoose";
|
|
4
|
-
import { Repository, aggregateHelpersPlugin, isDuplicateKeyError, methodRegistryPlugin, multiTenantPlugin } from "@classytic/mongokit";
|
|
5
|
-
import {
|
|
3
|
+
import { Repository, aggregateHelpersPlugin, isDuplicateKeyError, methodRegistryPlugin, multiTenantPlugin, withTransaction } from "@classytic/mongokit";
|
|
4
|
+
import { createEvent as createEvent$1, matchEventPattern } from "@classytic/primitives/events";
|
|
5
|
+
import { IllegalTransitionError, assertAndClaim, defineStateMachine } from "@classytic/primitives/state-machine";
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
7
|
import { z } from "zod";
|
|
8
|
+
import { civilDateOf } from "@classytic/primitives/timezone";
|
|
9
|
+
import { CURRENCY_PATTERN } from "@classytic/primitives/currency";
|
|
8
10
|
import { appendStatus, lastTransitionTo, latestEntry, timeInStatus } from "@classytic/primitives/status-history";
|
|
9
11
|
//#region src/core/errors.ts
|
|
10
12
|
/**
|
|
@@ -58,6 +60,11 @@ var DuplicateLeadError = class extends CrmError {
|
|
|
58
60
|
super("DUPLICATE_LEAD", `A lead with this email / phone already exists`, details);
|
|
59
61
|
}
|
|
60
62
|
};
|
|
63
|
+
var UnmanagedSessionError = class extends CrmError {
|
|
64
|
+
constructor(eventType) {
|
|
65
|
+
super("UNMANAGED_SESSION", "ctx.session was provided but no event delivery path exists — inside a host-owned transaction the event would either leak to subscribers before commit or be lost on rollback. Use engine.withTransaction(ctx, fn), wire CrmConfig.outbox, or attach a ctx[PENDING_EVENTS] queue and call flushPending() after commit (PACKAGE_RULES §P8.1).", eventType !== void 0 ? { eventType } : void 0);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
61
68
|
var CrmModelCollisionError = class extends CrmError {
|
|
62
69
|
constructor(modelName) {
|
|
63
70
|
super("CRM_MODEL_COLLISION", `Mongoose model '${modelName}' is already registered on this connection. Another engine (or a hot reload) owns it. Pass { forceRecreate: true } to createCrm() for test/hot-reload fixtures, or use a separate mongoose connection per engine in production (PACKAGE_RULES §21).`, { modelName });
|
|
@@ -98,6 +105,21 @@ var InvalidUnsubscribeTokenError = class extends CrmError {
|
|
|
98
105
|
super("INVALID_UNSUBSCRIBE_TOKEN", "No subscription matches the supplied unsubscribe token");
|
|
99
106
|
}
|
|
100
107
|
};
|
|
108
|
+
var InvalidSequenceTransitionError = class extends CrmError {
|
|
109
|
+
constructor(details) {
|
|
110
|
+
super("INVALID_SEQUENCE_TRANSITION", `Sequence ${details.sequenceId} cannot move from '${details.from}' to '${details.to}'`, details);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
var InvalidEnrollmentTransitionError = class extends CrmError {
|
|
114
|
+
constructor(details) {
|
|
115
|
+
super("INVALID_ENROLLMENT_TRANSITION", `Enrollment ${details.enrollmentId} cannot move from '${details.from}' to '${details.to}'`, details);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
var AlreadyEnrolledError = class extends CrmError {
|
|
119
|
+
constructor(details) {
|
|
120
|
+
super("ALREADY_ENROLLED", `Subject '${details.subjectId}' already has an active enrollment in sequence '${details.sequenceId}'`, details);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
101
123
|
//#endregion
|
|
102
124
|
//#region src/models/tenant-field.ts
|
|
103
125
|
/**
|
|
@@ -257,7 +279,7 @@ function canTransitionLead(from, to) {
|
|
|
257
279
|
/**
|
|
258
280
|
* Lead — top-of-funnel inquiry, pre-qualification.
|
|
259
281
|
*/
|
|
260
|
-
const statusEntrySchema$
|
|
282
|
+
const statusEntrySchema$5 = new Schema({
|
|
261
283
|
status: {
|
|
262
284
|
type: String,
|
|
263
285
|
enum: LEAD_STATUSES,
|
|
@@ -331,7 +353,7 @@ function buildLeadSchema(config) {
|
|
|
331
353
|
index: true
|
|
332
354
|
},
|
|
333
355
|
statusHistory: {
|
|
334
|
-
type: [statusEntrySchema$
|
|
356
|
+
type: [statusEntrySchema$5],
|
|
335
357
|
default: []
|
|
336
358
|
},
|
|
337
359
|
ownerId: {
|
|
@@ -418,7 +440,7 @@ function canTransitionOpportunity(from, to) {
|
|
|
418
440
|
* Money amount lives flat (`amount` + `currency`) so `$sum` aggregates work; the repo
|
|
419
441
|
* surface returns it that way too (no Money translation layer per PACKAGE_RULES §4).
|
|
420
442
|
*/
|
|
421
|
-
const statusEntrySchema$
|
|
443
|
+
const statusEntrySchema$4 = new Schema({
|
|
422
444
|
status: {
|
|
423
445
|
type: String,
|
|
424
446
|
enum: OPPORTUNITY_STATUSES,
|
|
@@ -466,7 +488,7 @@ function buildOpportunitySchema(config) {
|
|
|
466
488
|
index: true
|
|
467
489
|
},
|
|
468
490
|
statusHistory: {
|
|
469
|
-
type: [statusEntrySchema$
|
|
491
|
+
type: [statusEntrySchema$4],
|
|
470
492
|
default: []
|
|
471
493
|
},
|
|
472
494
|
amount: { type: Number },
|
|
@@ -779,7 +801,7 @@ function canTransitionCampaign(from, to) {
|
|
|
779
801
|
* Campaign — a marketing/sales motion targeting a contact list.
|
|
780
802
|
* Money fields stored flat (amount + currency) for $sum aggregations.
|
|
781
803
|
*/
|
|
782
|
-
const statusEntrySchema$
|
|
804
|
+
const statusEntrySchema$3 = new Schema({
|
|
783
805
|
status: {
|
|
784
806
|
type: String,
|
|
785
807
|
enum: CAMPAIGN_STATUSES,
|
|
@@ -813,7 +835,7 @@ function buildCampaignSchema(config) {
|
|
|
813
835
|
index: true
|
|
814
836
|
},
|
|
815
837
|
statusHistory: {
|
|
816
|
-
type: [statusEntrySchema$
|
|
838
|
+
type: [statusEntrySchema$3],
|
|
817
839
|
default: []
|
|
818
840
|
},
|
|
819
841
|
utmSource: {
|
|
@@ -928,7 +950,7 @@ function canTransitionSubscription(from, to) {
|
|
|
928
950
|
* same connection. TypeScript class + file names retain `Subscription` for
|
|
929
951
|
* source-level continuity — only the Mongo identity changed in v0.4.0.
|
|
930
952
|
*/
|
|
931
|
-
const statusEntrySchema = new Schema({
|
|
953
|
+
const statusEntrySchema$2 = new Schema({
|
|
932
954
|
status: {
|
|
933
955
|
type: String,
|
|
934
956
|
enum: SUBSCRIPTION_STATUSES,
|
|
@@ -961,7 +983,7 @@ function buildSubscriptionSchema(config) {
|
|
|
961
983
|
index: true
|
|
962
984
|
},
|
|
963
985
|
statusHistory: {
|
|
964
|
-
type: [statusEntrySchema],
|
|
986
|
+
type: [statusEntrySchema$2],
|
|
965
987
|
default: []
|
|
966
988
|
},
|
|
967
989
|
source: {
|
|
@@ -1036,6 +1058,83 @@ function createSubscriptionModel(connection, config) {
|
|
|
1036
1058
|
return connection.model("EmailSubscription", buildSubscriptionSchema(config));
|
|
1037
1059
|
}
|
|
1038
1060
|
//#endregion
|
|
1061
|
+
//#region src/enums/suppression-reason.ts
|
|
1062
|
+
/** Reasons derived 1:1 from an email engagement event kind. */
|
|
1063
|
+
const EVENT_DERIVED_SUPPRESSION_REASONS = [
|
|
1064
|
+
"bounce",
|
|
1065
|
+
"complaint",
|
|
1066
|
+
"unsubscribe"
|
|
1067
|
+
];
|
|
1068
|
+
const SUPPRESSION_REASONS = [...EVENT_DERIVED_SUPPRESSION_REASONS, "manual"];
|
|
1069
|
+
//#endregion
|
|
1070
|
+
//#region src/models/suppression.schema.ts
|
|
1071
|
+
/**
|
|
1072
|
+
* Suppression — org-wide "never email this address again" list.
|
|
1073
|
+
*
|
|
1074
|
+
* Subscription unsubscribes are per-campaign; a Suppression row is the
|
|
1075
|
+
* GLOBAL gate a host consults before EVERY outbound email (campaign
|
|
1076
|
+
* fanout, sequence step sends, one-off notifications). One row per
|
|
1077
|
+
* (org, email) — `suppress()` is getOrCreate-idempotent against the
|
|
1078
|
+
* unique index below.
|
|
1079
|
+
*
|
|
1080
|
+
* `subjectKind`/`subjectId` are optional String external refs (rule 7 —
|
|
1081
|
+
* polymorphic across Contact/Lead/host-owned models, so no
|
|
1082
|
+
* ObjectId+refPath). `source` is a free-form origin marker, e.g.
|
|
1083
|
+
* `'campaign:<id>'`, `'sequence:<id>'`, `'api'`, `'import'`.
|
|
1084
|
+
*/
|
|
1085
|
+
function buildSuppressionSchema(config) {
|
|
1086
|
+
const schema = new Schema({
|
|
1087
|
+
organizationId: tenantFieldDef(config),
|
|
1088
|
+
email: {
|
|
1089
|
+
type: String,
|
|
1090
|
+
required: true,
|
|
1091
|
+
trim: true,
|
|
1092
|
+
lowercase: true,
|
|
1093
|
+
maxlength: 320
|
|
1094
|
+
},
|
|
1095
|
+
reason: {
|
|
1096
|
+
type: String,
|
|
1097
|
+
enum: SUPPRESSION_REASONS,
|
|
1098
|
+
required: true
|
|
1099
|
+
},
|
|
1100
|
+
source: {
|
|
1101
|
+
type: String,
|
|
1102
|
+
trim: true,
|
|
1103
|
+
maxlength: 200
|
|
1104
|
+
},
|
|
1105
|
+
subjectKind: {
|
|
1106
|
+
type: String,
|
|
1107
|
+
trim: true,
|
|
1108
|
+
maxlength: 50
|
|
1109
|
+
},
|
|
1110
|
+
subjectId: {
|
|
1111
|
+
type: String,
|
|
1112
|
+
trim: true,
|
|
1113
|
+
maxlength: 100
|
|
1114
|
+
},
|
|
1115
|
+
suppressedAt: {
|
|
1116
|
+
type: Date,
|
|
1117
|
+
required: true,
|
|
1118
|
+
default: () => /* @__PURE__ */ new Date()
|
|
1119
|
+
},
|
|
1120
|
+
metadata: { type: Schema.Types.Mixed }
|
|
1121
|
+
}, { timestamps: true });
|
|
1122
|
+
schema.index({
|
|
1123
|
+
organizationId: 1,
|
|
1124
|
+
email: 1
|
|
1125
|
+
}, { unique: true });
|
|
1126
|
+
schema.index({
|
|
1127
|
+
organizationId: 1,
|
|
1128
|
+
reason: 1,
|
|
1129
|
+
suppressedAt: -1
|
|
1130
|
+
});
|
|
1131
|
+
schema.index({
|
|
1132
|
+
organizationId: 1,
|
|
1133
|
+
suppressedAt: -1
|
|
1134
|
+
});
|
|
1135
|
+
return schema;
|
|
1136
|
+
}
|
|
1137
|
+
//#endregion
|
|
1039
1138
|
//#region src/enums/activity.ts
|
|
1040
1139
|
/**
|
|
1041
1140
|
* Activity type vocabulary. The package stores `type` as free-form
|
|
@@ -1608,6 +1707,263 @@ function createMessageModel(connection, config) {
|
|
|
1608
1707
|
return connection.model("Message", buildMessageSchema(config));
|
|
1609
1708
|
}
|
|
1610
1709
|
//#endregion
|
|
1710
|
+
//#region src/enums/sequence-status.ts
|
|
1711
|
+
/**
|
|
1712
|
+
* Email sequence lifecycle.
|
|
1713
|
+
*
|
|
1714
|
+
* draft — being built; no enrollments can be created
|
|
1715
|
+
* active — accepting new enrollments; steps are executing
|
|
1716
|
+
* paused — no new enrollments; running enrollments pause at next step
|
|
1717
|
+
* archived — soft-hidden; historical data preserved
|
|
1718
|
+
*/
|
|
1719
|
+
const SEQUENCE_STATUSES = [
|
|
1720
|
+
"draft",
|
|
1721
|
+
"active",
|
|
1722
|
+
"paused",
|
|
1723
|
+
"archived"
|
|
1724
|
+
];
|
|
1725
|
+
const SEQUENCE_TRANSITIONS = {
|
|
1726
|
+
draft: ["active", "archived"],
|
|
1727
|
+
active: ["paused", "archived"],
|
|
1728
|
+
paused: ["active", "archived"],
|
|
1729
|
+
archived: []
|
|
1730
|
+
};
|
|
1731
|
+
function canTransitionSequence(from, to) {
|
|
1732
|
+
return SEQUENCE_TRANSITIONS[from].includes(to);
|
|
1733
|
+
}
|
|
1734
|
+
//#endregion
|
|
1735
|
+
//#region src/models/sequence.schema.ts
|
|
1736
|
+
/**
|
|
1737
|
+
* Sequence — ordered cadence of SEND_EMAIL and DELAY steps.
|
|
1738
|
+
*
|
|
1739
|
+
* Steps are stored as a JSON array (Mixed) so the host can add new step
|
|
1740
|
+
* types without a schema migration. The discriminator is `step.type`.
|
|
1741
|
+
*/
|
|
1742
|
+
const statusEntrySchema$1 = new Schema({
|
|
1743
|
+
status: {
|
|
1744
|
+
type: String,
|
|
1745
|
+
enum: SEQUENCE_STATUSES,
|
|
1746
|
+
required: true
|
|
1747
|
+
},
|
|
1748
|
+
occurredAt: {
|
|
1749
|
+
type: Date,
|
|
1750
|
+
required: true
|
|
1751
|
+
},
|
|
1752
|
+
by: { type: String },
|
|
1753
|
+
note: { type: String }
|
|
1754
|
+
}, { _id: false });
|
|
1755
|
+
function buildSequenceSchema(config) {
|
|
1756
|
+
const schema = new Schema({
|
|
1757
|
+
organizationId: tenantFieldDef(config),
|
|
1758
|
+
name: {
|
|
1759
|
+
type: String,
|
|
1760
|
+
required: true,
|
|
1761
|
+
trim: true,
|
|
1762
|
+
maxlength: 200
|
|
1763
|
+
},
|
|
1764
|
+
description: {
|
|
1765
|
+
type: String,
|
|
1766
|
+
trim: true,
|
|
1767
|
+
maxlength: 1e3
|
|
1768
|
+
},
|
|
1769
|
+
status: {
|
|
1770
|
+
type: String,
|
|
1771
|
+
enum: SEQUENCE_STATUSES,
|
|
1772
|
+
default: "draft",
|
|
1773
|
+
required: true
|
|
1774
|
+
},
|
|
1775
|
+
fromName: {
|
|
1776
|
+
type: String,
|
|
1777
|
+
trim: true,
|
|
1778
|
+
maxlength: 100
|
|
1779
|
+
},
|
|
1780
|
+
fromEmail: {
|
|
1781
|
+
type: String,
|
|
1782
|
+
trim: true,
|
|
1783
|
+
maxlength: 200
|
|
1784
|
+
},
|
|
1785
|
+
steps: {
|
|
1786
|
+
type: [Schema.Types.Mixed],
|
|
1787
|
+
default: []
|
|
1788
|
+
},
|
|
1789
|
+
replyDetectionEnabled: {
|
|
1790
|
+
type: Boolean,
|
|
1791
|
+
default: true
|
|
1792
|
+
},
|
|
1793
|
+
tags: {
|
|
1794
|
+
type: [String],
|
|
1795
|
+
default: []
|
|
1796
|
+
},
|
|
1797
|
+
ownerId: { type: String },
|
|
1798
|
+
metadata: { type: Schema.Types.Mixed },
|
|
1799
|
+
statusHistory: {
|
|
1800
|
+
type: [statusEntrySchema$1],
|
|
1801
|
+
default: []
|
|
1802
|
+
}
|
|
1803
|
+
}, { timestamps: true });
|
|
1804
|
+
schema.index({
|
|
1805
|
+
organizationId: 1,
|
|
1806
|
+
status: 1
|
|
1807
|
+
});
|
|
1808
|
+
schema.index({
|
|
1809
|
+
organizationId: 1,
|
|
1810
|
+
createdAt: -1
|
|
1811
|
+
});
|
|
1812
|
+
return schema;
|
|
1813
|
+
}
|
|
1814
|
+
//#endregion
|
|
1815
|
+
//#region src/enums/enrollment-status.ts
|
|
1816
|
+
/**
|
|
1817
|
+
* Enrollment lifecycle.
|
|
1818
|
+
*
|
|
1819
|
+
* active — steps are executing (or waiting on a delay)
|
|
1820
|
+
* paused — execution halted; can be resumed
|
|
1821
|
+
* completed — all steps ran to completion
|
|
1822
|
+
* exited — removed early (reply, unsubscribe, bounce, manual)
|
|
1823
|
+
*/
|
|
1824
|
+
const ENROLLMENT_STATUSES = [
|
|
1825
|
+
"active",
|
|
1826
|
+
"paused",
|
|
1827
|
+
"completed",
|
|
1828
|
+
"exited"
|
|
1829
|
+
];
|
|
1830
|
+
const ENROLLMENT_TRANSITIONS = {
|
|
1831
|
+
active: [
|
|
1832
|
+
"paused",
|
|
1833
|
+
"completed",
|
|
1834
|
+
"exited"
|
|
1835
|
+
],
|
|
1836
|
+
paused: ["active", "exited"],
|
|
1837
|
+
completed: [],
|
|
1838
|
+
exited: []
|
|
1839
|
+
};
|
|
1840
|
+
function canTransitionEnrollment(from, to) {
|
|
1841
|
+
return ENROLLMENT_TRANSITIONS[from].includes(to);
|
|
1842
|
+
}
|
|
1843
|
+
//#endregion
|
|
1844
|
+
//#region src/models/enrollment.schema.ts
|
|
1845
|
+
/**
|
|
1846
|
+
* Enrollment — a single subject (lead or contact) progressing through a Sequence.
|
|
1847
|
+
*
|
|
1848
|
+
* `currentStepIndex` tracks which step to execute next. The sequence workflow
|
|
1849
|
+
* reads this field at each iteration so crashes / pauses resume from the
|
|
1850
|
+
* correct position without re-running completed steps.
|
|
1851
|
+
*/
|
|
1852
|
+
const ENROLLMENT_SUBJECT_KINDS = ["lead", "contact"];
|
|
1853
|
+
const stepResultSchema = new Schema({
|
|
1854
|
+
stepId: {
|
|
1855
|
+
type: String,
|
|
1856
|
+
required: true
|
|
1857
|
+
},
|
|
1858
|
+
stepType: {
|
|
1859
|
+
type: String,
|
|
1860
|
+
required: true
|
|
1861
|
+
},
|
|
1862
|
+
status: {
|
|
1863
|
+
type: String,
|
|
1864
|
+
enum: [
|
|
1865
|
+
"pending",
|
|
1866
|
+
"sent",
|
|
1867
|
+
"failed",
|
|
1868
|
+
"skipped"
|
|
1869
|
+
],
|
|
1870
|
+
required: true
|
|
1871
|
+
},
|
|
1872
|
+
sentAt: { type: Date },
|
|
1873
|
+
error: { type: String }
|
|
1874
|
+
}, { _id: false });
|
|
1875
|
+
const statusEntrySchema = new Schema({
|
|
1876
|
+
status: {
|
|
1877
|
+
type: String,
|
|
1878
|
+
enum: ENROLLMENT_STATUSES,
|
|
1879
|
+
required: true
|
|
1880
|
+
},
|
|
1881
|
+
occurredAt: {
|
|
1882
|
+
type: Date,
|
|
1883
|
+
required: true
|
|
1884
|
+
},
|
|
1885
|
+
by: { type: String }
|
|
1886
|
+
}, { _id: false });
|
|
1887
|
+
function buildEnrollmentSchema(config) {
|
|
1888
|
+
const schema = new Schema({
|
|
1889
|
+
organizationId: tenantFieldDef(config),
|
|
1890
|
+
sequenceId: {
|
|
1891
|
+
type: config.tenantFieldType === "objectId" ? Schema.Types.ObjectId : String,
|
|
1892
|
+
required: true,
|
|
1893
|
+
ref: "Sequence"
|
|
1894
|
+
},
|
|
1895
|
+
subjectKind: {
|
|
1896
|
+
type: String,
|
|
1897
|
+
enum: ENROLLMENT_SUBJECT_KINDS,
|
|
1898
|
+
required: true
|
|
1899
|
+
},
|
|
1900
|
+
subjectId: {
|
|
1901
|
+
type: config.tenantFieldType === "objectId" ? Schema.Types.ObjectId : String,
|
|
1902
|
+
required: true
|
|
1903
|
+
},
|
|
1904
|
+
status: {
|
|
1905
|
+
type: String,
|
|
1906
|
+
enum: ENROLLMENT_STATUSES,
|
|
1907
|
+
default: "active",
|
|
1908
|
+
required: true
|
|
1909
|
+
},
|
|
1910
|
+
currentStepIndex: {
|
|
1911
|
+
type: Number,
|
|
1912
|
+
default: 0,
|
|
1913
|
+
min: 0
|
|
1914
|
+
},
|
|
1915
|
+
stepResults: {
|
|
1916
|
+
type: [stepResultSchema],
|
|
1917
|
+
default: []
|
|
1918
|
+
},
|
|
1919
|
+
resumeAt: { type: Date },
|
|
1920
|
+
workflowRunId: { type: String },
|
|
1921
|
+
exitReason: {
|
|
1922
|
+
type: String,
|
|
1923
|
+
enum: [
|
|
1924
|
+
"completed",
|
|
1925
|
+
"reply",
|
|
1926
|
+
"unsubscribe",
|
|
1927
|
+
"bounce",
|
|
1928
|
+
"manual"
|
|
1929
|
+
]
|
|
1930
|
+
},
|
|
1931
|
+
enrolledAt: {
|
|
1932
|
+
type: Date,
|
|
1933
|
+
required: true,
|
|
1934
|
+
default: () => /* @__PURE__ */ new Date()
|
|
1935
|
+
},
|
|
1936
|
+
completedAt: { type: Date },
|
|
1937
|
+
ownerId: { type: String },
|
|
1938
|
+
metadata: { type: Schema.Types.Mixed },
|
|
1939
|
+
statusHistory: {
|
|
1940
|
+
type: [statusEntrySchema],
|
|
1941
|
+
default: []
|
|
1942
|
+
}
|
|
1943
|
+
}, { timestamps: true });
|
|
1944
|
+
schema.index({
|
|
1945
|
+
organizationId: 1,
|
|
1946
|
+
sequenceId: 1,
|
|
1947
|
+
subjectId: 1,
|
|
1948
|
+
status: 1
|
|
1949
|
+
}, { unique: false });
|
|
1950
|
+
schema.index({
|
|
1951
|
+
organizationId: 1,
|
|
1952
|
+
sequenceId: 1,
|
|
1953
|
+
status: 1
|
|
1954
|
+
});
|
|
1955
|
+
schema.index({
|
|
1956
|
+
organizationId: 1,
|
|
1957
|
+
subjectKind: 1,
|
|
1958
|
+
subjectId: 1
|
|
1959
|
+
});
|
|
1960
|
+
schema.index({
|
|
1961
|
+
organizationId: 1,
|
|
1962
|
+
createdAt: -1
|
|
1963
|
+
});
|
|
1964
|
+
return schema;
|
|
1965
|
+
}
|
|
1966
|
+
//#endregion
|
|
1611
1967
|
//#region src/models/create-models.ts
|
|
1612
1968
|
/** Mongoose model names this package registers — drives the §21 collision check. */
|
|
1613
1969
|
const CRM_MODEL_NAMES = [
|
|
@@ -1618,12 +1974,15 @@ const CRM_MODEL_NAMES = [
|
|
|
1618
1974
|
"Account",
|
|
1619
1975
|
"Campaign",
|
|
1620
1976
|
"EmailSubscription",
|
|
1977
|
+
"Suppression",
|
|
1621
1978
|
"Activity",
|
|
1622
1979
|
"Note",
|
|
1623
1980
|
"LossReason",
|
|
1624
1981
|
"EmailEvent",
|
|
1625
1982
|
"Conversation",
|
|
1626
|
-
"Message"
|
|
1983
|
+
"Message",
|
|
1984
|
+
"Sequence",
|
|
1985
|
+
"Enrollment"
|
|
1627
1986
|
];
|
|
1628
1987
|
function createCrmModels(connection, config) {
|
|
1629
1988
|
const forceRecreate = config.forceRecreate ?? false;
|
|
@@ -1644,12 +2003,107 @@ function createCrmModels(connection, config) {
|
|
|
1644
2003
|
Account: register("Account", buildAccountSchema(config)),
|
|
1645
2004
|
Campaign: register("Campaign", buildCampaignSchema(config)),
|
|
1646
2005
|
Subscription: register("EmailSubscription", buildSubscriptionSchema(config)),
|
|
2006
|
+
Suppression: register("Suppression", buildSuppressionSchema(config)),
|
|
1647
2007
|
Activity: register("Activity", buildActivitySchema(config)),
|
|
1648
2008
|
Note: register("Note", buildNoteSchema(config)),
|
|
1649
2009
|
LossReason: register("LossReason", buildLossReasonSchema(config)),
|
|
1650
2010
|
EmailEvent: register("EmailEvent", buildEmailEventSchema(config)),
|
|
1651
2011
|
Conversation: register("Conversation", buildConversationSchema(config)),
|
|
1652
|
-
Message: register("Message", buildMessageSchema(config))
|
|
2012
|
+
Message: register("Message", buildMessageSchema(config)),
|
|
2013
|
+
Sequence: register("Sequence", buildSequenceSchema(config)),
|
|
2014
|
+
Enrollment: register("Enrollment", buildEnrollmentSchema(config))
|
|
2015
|
+
});
|
|
2016
|
+
}
|
|
2017
|
+
//#endregion
|
|
2018
|
+
//#region src/events/event-constants.ts
|
|
2019
|
+
/** PACKAGE_RULES rule 12: `package:resource.verb`. */
|
|
2020
|
+
const CRM_EVENTS = {
|
|
2021
|
+
LEAD_CREATED: "crm:lead.created",
|
|
2022
|
+
LEAD_STATUS_CHANGED: "crm:lead.status_changed",
|
|
2023
|
+
LEAD_SCORED: "crm:lead.scored",
|
|
2024
|
+
LEAD_CONVERTED: "crm:lead.converted",
|
|
2025
|
+
LEAD_DISQUALIFIED: "crm:lead.disqualified",
|
|
2026
|
+
ACCOUNT_CREATED: "crm:account.created",
|
|
2027
|
+
ACCOUNT_UPDATED: "crm:account.updated",
|
|
2028
|
+
CONTACT_CREATED: "crm:contact.created",
|
|
2029
|
+
CONTACT_UPDATED: "crm:contact.updated",
|
|
2030
|
+
OPPORTUNITY_CREATED: "crm:opportunity.created",
|
|
2031
|
+
OPPORTUNITY_STAGE_ADVANCED: "crm:opportunity.stage_advanced",
|
|
2032
|
+
OPPORTUNITY_STAGE_REGRESSED: "crm:opportunity.stage_regressed",
|
|
2033
|
+
OPPORTUNITY_WON: "crm:opportunity.won",
|
|
2034
|
+
OPPORTUNITY_LOST: "crm:opportunity.lost",
|
|
2035
|
+
OPPORTUNITY_ABANDONED: "crm:opportunity.abandoned",
|
|
2036
|
+
PIPELINE_CREATED: "crm:pipeline.created",
|
|
2037
|
+
PIPELINE_ARCHIVED: "crm:pipeline.archived",
|
|
2038
|
+
ACTIVITY_PLANNED: "crm:activity.planned",
|
|
2039
|
+
ACTIVITY_COMPLETED: "crm:activity.completed",
|
|
2040
|
+
ACTIVITY_CANCELLED: "crm:activity.cancelled",
|
|
2041
|
+
NOTE_ADDED: "crm:note.added",
|
|
2042
|
+
CAMPAIGN_CREATED: "crm:campaign.created",
|
|
2043
|
+
CAMPAIGN_ACTIVATED: "crm:campaign.activated",
|
|
2044
|
+
CAMPAIGN_PAUSED: "crm:campaign.paused",
|
|
2045
|
+
CAMPAIGN_RESUMED: "crm:campaign.resumed",
|
|
2046
|
+
CAMPAIGN_COMPLETED: "crm:campaign.completed",
|
|
2047
|
+
CAMPAIGN_ARCHIVED: "crm:campaign.archived",
|
|
2048
|
+
SUBSCRIPTION_CREATED: "crm:subscription.created",
|
|
2049
|
+
SUBSCRIPTION_CONFIRMED: "crm:subscription.confirmed",
|
|
2050
|
+
SUBSCRIPTION_UNSUBSCRIBED: "crm:subscription.unsubscribed",
|
|
2051
|
+
SUBSCRIPTION_BOUNCED: "crm:subscription.bounced",
|
|
2052
|
+
SUBSCRIPTION_COMPLAINED: "crm:subscription.complained",
|
|
2053
|
+
SEQUENCE_CREATED: "crm:sequence.created",
|
|
2054
|
+
SEQUENCE_ACTIVATED: "crm:sequence.active",
|
|
2055
|
+
SEQUENCE_PAUSED: "crm:sequence.paused",
|
|
2056
|
+
SEQUENCE_ARCHIVED: "crm:sequence.archived",
|
|
2057
|
+
ENROLLMENT_CREATED: "crm:enrollment.created",
|
|
2058
|
+
ENROLLMENT_STEP_SENT: "crm:enrollment.step_sent",
|
|
2059
|
+
ENROLLMENT_PAUSED: "crm:enrollment.paused",
|
|
2060
|
+
ENROLLMENT_RESUMED: "crm:enrollment.resumed",
|
|
2061
|
+
ENROLLMENT_COMPLETED: "crm:enrollment.completed",
|
|
2062
|
+
ENROLLMENT_EXITED: "crm:enrollment.exited",
|
|
2063
|
+
EMAIL_EVENT_OPENED: "crm:email_event.opened",
|
|
2064
|
+
EMAIL_EVENT_CLICKED: "crm:email_event.clicked",
|
|
2065
|
+
EMAIL_EVENT_BOUNCED: "crm:email_event.bounced",
|
|
2066
|
+
EMAIL_EVENT_COMPLAINED: "crm:email_event.complained",
|
|
2067
|
+
EMAIL_EVENT_UNSUBSCRIBED: "crm:email_event.unsubscribed",
|
|
2068
|
+
LEAD_ENRICHED: "crm:lead.enriched",
|
|
2069
|
+
LEAD_ASSIGNED: "crm:lead.assigned",
|
|
2070
|
+
CONTACT_ENRICHED: "crm:contact.enriched",
|
|
2071
|
+
CONTACT_ASSIGNED: "crm:contact.assigned",
|
|
2072
|
+
CONTACT_MERGED: "crm:contact.merged",
|
|
2073
|
+
ACCOUNT_ENRICHED: "crm:account.enriched",
|
|
2074
|
+
ACCOUNT_ASSIGNED: "crm:account.assigned",
|
|
2075
|
+
ACCOUNT_MERGED: "crm:account.merged",
|
|
2076
|
+
OPPORTUNITY_ASSIGNED: "crm:opportunity.assigned",
|
|
2077
|
+
OPPORTUNITY_AMOUNT_CHANGED: "crm:opportunity.amount_changed",
|
|
2078
|
+
OPPORTUNITY_EXTENDED: "crm:opportunity.extended",
|
|
2079
|
+
ACTIVITY_RESCHEDULED: "crm:activity.rescheduled",
|
|
2080
|
+
ACTIVITY_OUTCOME_RECORDED: "crm:activity.outcome_recorded",
|
|
2081
|
+
SUPPRESSION_ADDED: "crm:suppression.added",
|
|
2082
|
+
SUPPRESSION_REMOVED: "crm:suppression.removed",
|
|
2083
|
+
CONVERSATION_CREATED: "crm:conversation.created",
|
|
2084
|
+
CONVERSATION_ASSIGNED: "crm:conversation.assigned",
|
|
2085
|
+
CONVERSATION_RESOLVED: "crm:conversation.resolved",
|
|
2086
|
+
CONVERSATION_REOPENED: "crm:conversation.reopened",
|
|
2087
|
+
MESSAGE_RECEIVED: "crm:message.received",
|
|
2088
|
+
MESSAGE_SENT: "crm:message.sent"
|
|
2089
|
+
};
|
|
2090
|
+
//#endregion
|
|
2091
|
+
//#region src/events/helpers.ts
|
|
2092
|
+
/**
|
|
2093
|
+
* Event creation helper.
|
|
2094
|
+
*
|
|
2095
|
+
* Thin wrapper around `@classytic/primitives`' `createEvent` that fills the
|
|
2096
|
+
* shared `EventMeta` from a `CrmContext` (userId/actorId, organizationId,
|
|
2097
|
+
* correlationId). Use this instead of importing `createEvent` directly so
|
|
2098
|
+
* the `source: 'crm'` stamp + tenant scoping stay consistent.
|
|
2099
|
+
*/
|
|
2100
|
+
function createEvent(type, payload, ctx, meta) {
|
|
2101
|
+
return createEvent$1(type, payload, {
|
|
2102
|
+
source: "crm",
|
|
2103
|
+
...ctx?.actorId !== void 0 ? { actorId: String(ctx.actorId) } : {},
|
|
2104
|
+
...ctx?.organizationId !== void 0 ? { organizationId: String(ctx.organizationId) } : {},
|
|
2105
|
+
...ctx?.correlationId !== void 0 ? { correlationId: ctx.correlationId } : {},
|
|
2106
|
+
...meta
|
|
1653
2107
|
});
|
|
1654
2108
|
}
|
|
1655
2109
|
//#endregion
|
|
@@ -1698,7 +2152,7 @@ var PipelineRepository = class extends Repository {
|
|
|
1698
2152
|
async archive(id, ctx) {
|
|
1699
2153
|
const updated = await this.update(id, { isArchived: true }, ctx);
|
|
1700
2154
|
if (!updated) throw new NotFoundError("Pipeline", id);
|
|
1701
|
-
await this.events.
|
|
2155
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.PIPELINE_ARCHIVED, { pipelineId: id }, ctx));
|
|
1702
2156
|
return updated;
|
|
1703
2157
|
}
|
|
1704
2158
|
};
|
|
@@ -1764,9 +2218,27 @@ const ACTIVITY_MACHINE = defineStateMachine({
|
|
|
1764
2218
|
activityId: entityId
|
|
1765
2219
|
})
|
|
1766
2220
|
});
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
2221
|
+
const SEQUENCE_MACHINE = defineStateMachine({
|
|
2222
|
+
name: "Sequence",
|
|
2223
|
+
transitions: SEQUENCE_TRANSITIONS,
|
|
2224
|
+
errorFactory: ({ entityId, from, to }) => new InvalidSequenceTransitionError({
|
|
2225
|
+
from,
|
|
2226
|
+
to,
|
|
2227
|
+
sequenceId: entityId
|
|
2228
|
+
})
|
|
2229
|
+
});
|
|
2230
|
+
const ENROLLMENT_MACHINE = defineStateMachine({
|
|
2231
|
+
name: "Enrollment",
|
|
2232
|
+
transitions: ENROLLMENT_TRANSITIONS,
|
|
2233
|
+
errorFactory: ({ entityId, from, to }) => new InvalidEnrollmentTransitionError({
|
|
2234
|
+
from,
|
|
2235
|
+
to,
|
|
2236
|
+
enrollmentId: entityId
|
|
2237
|
+
})
|
|
2238
|
+
});
|
|
2239
|
+
//#endregion
|
|
2240
|
+
//#region src/shared/calculators/scoring.ts
|
|
2241
|
+
/**
|
|
1770
2242
|
* Reference implementation. Intentionally simple — hosts read it and
|
|
1771
2243
|
* replace it.
|
|
1772
2244
|
*
|
|
@@ -1833,12 +2305,12 @@ var LeadRepository = class extends Repository {
|
|
|
1833
2305
|
let result = created;
|
|
1834
2306
|
if (score !== 0) {
|
|
1835
2307
|
result = await this.update(String(created._id), { score }, ctx) ?? created;
|
|
1836
|
-
await this.events.
|
|
2308
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.LEAD_SCORED, {
|
|
1837
2309
|
leadId: String(created._id),
|
|
1838
2310
|
score
|
|
1839
2311
|
}, ctx));
|
|
1840
2312
|
}
|
|
1841
|
-
await this.events.
|
|
2313
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.LEAD_CREATED, {
|
|
1842
2314
|
leadId: String(result._id),
|
|
1843
2315
|
email: result.email,
|
|
1844
2316
|
score: result.score
|
|
@@ -1857,7 +2329,7 @@ var LeadRepository = class extends Repository {
|
|
|
1857
2329
|
note: reason,
|
|
1858
2330
|
patch: { disqualifyReason: reason }
|
|
1859
2331
|
}, ctx);
|
|
1860
|
-
await this.events.
|
|
2332
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.LEAD_DISQUALIFIED, {
|
|
1861
2333
|
leadId: id,
|
|
1862
2334
|
reason
|
|
1863
2335
|
}, ctx));
|
|
@@ -1879,7 +2351,7 @@ var LeadRepository = class extends Repository {
|
|
|
1879
2351
|
if (!await this.getById(id, ctx)) throw new NotFoundError("Lead", id);
|
|
1880
2352
|
if (!await this.update(id, patch, ctx)) throw new NotFoundError("Lead", id);
|
|
1881
2353
|
const fields = Object.keys(patch);
|
|
1882
|
-
await this.events.
|
|
2354
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.LEAD_ENRICHED, {
|
|
1883
2355
|
leadId: id,
|
|
1884
2356
|
fields,
|
|
1885
2357
|
...extras.provider !== void 0 ? { provider: extras.provider } : {}
|
|
@@ -1903,7 +2375,7 @@ var LeadRepository = class extends Repository {
|
|
|
1903
2375
|
if (!current) throw new NotFoundError("Lead", id);
|
|
1904
2376
|
return current;
|
|
1905
2377
|
}
|
|
1906
|
-
await this.events.
|
|
2378
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.LEAD_ASSIGNED, {
|
|
1907
2379
|
leadId: id,
|
|
1908
2380
|
ownerId,
|
|
1909
2381
|
...before.ownerId !== void 0 ? { previousOwnerId: before.ownerId } : {}
|
|
@@ -1921,7 +2393,7 @@ var LeadRepository = class extends Repository {
|
|
|
1921
2393
|
if (score === lead.score) return lead;
|
|
1922
2394
|
const updated = await this.update(id, { score }, ctx);
|
|
1923
2395
|
if (!updated) throw new NotFoundError("Lead", id);
|
|
1924
|
-
await this.events.
|
|
2396
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.LEAD_SCORED, {
|
|
1925
2397
|
leadId: id,
|
|
1926
2398
|
score
|
|
1927
2399
|
}, ctx));
|
|
@@ -1951,7 +2423,7 @@ var LeadRepository = class extends Repository {
|
|
|
1951
2423
|
to: target,
|
|
1952
2424
|
leadId: id
|
|
1953
2425
|
});
|
|
1954
|
-
await this.events.
|
|
2426
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.LEAD_STATUS_CHANGED, {
|
|
1955
2427
|
leadId: id,
|
|
1956
2428
|
from: existing.status,
|
|
1957
2429
|
to: target
|
|
@@ -2017,7 +2489,7 @@ var OpportunityRepository = class extends Repository {
|
|
|
2017
2489
|
...input.metadata !== void 0 ? { metadata: input.metadata } : {}
|
|
2018
2490
|
};
|
|
2019
2491
|
const created = await this.create(draft, ctx);
|
|
2020
|
-
await this.events.
|
|
2492
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.OPPORTUNITY_CREATED, {
|
|
2021
2493
|
opportunityId: String(created._id),
|
|
2022
2494
|
pipelineId: input.pipelineId,
|
|
2023
2495
|
stageId: String(stage._id)
|
|
@@ -2061,7 +2533,7 @@ var OpportunityRepository = class extends Repository {
|
|
|
2061
2533
|
stageId: String(target._id)
|
|
2062
2534
|
});
|
|
2063
2535
|
const event = target.sequence > current.sequence ? CRM_EVENTS.OPPORTUNITY_STAGE_ADVANCED : CRM_EVENTS.OPPORTUNITY_STAGE_REGRESSED;
|
|
2064
|
-
await this.events.
|
|
2536
|
+
await this.events.dispatch(ctx, createEvent(event, {
|
|
2065
2537
|
opportunityId: id,
|
|
2066
2538
|
from: {
|
|
2067
2539
|
stageId: String(current._id),
|
|
@@ -2083,7 +2555,7 @@ var OpportunityRepository = class extends Repository {
|
|
|
2083
2555
|
...input.by !== void 0 ? { by: input.by } : {},
|
|
2084
2556
|
...input.note !== void 0 ? { note: input.note } : {}
|
|
2085
2557
|
}, ctx);
|
|
2086
|
-
await this.events.
|
|
2558
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.OPPORTUNITY_WON, {
|
|
2087
2559
|
opportunityId: id,
|
|
2088
2560
|
amount: updated.amount
|
|
2089
2561
|
}, ctx));
|
|
@@ -2098,7 +2570,7 @@ var OpportunityRepository = class extends Repository {
|
|
|
2098
2570
|
...input.by !== void 0 ? { by: input.by } : {},
|
|
2099
2571
|
...input.note !== void 0 ? { note: input.note } : {}
|
|
2100
2572
|
}, ctx);
|
|
2101
|
-
await this.events.
|
|
2573
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.OPPORTUNITY_LOST, {
|
|
2102
2574
|
opportunityId: id,
|
|
2103
2575
|
lostReasonId: updated.lostReasonId
|
|
2104
2576
|
}, ctx));
|
|
@@ -2121,7 +2593,7 @@ var OpportunityRepository = class extends Repository {
|
|
|
2121
2593
|
if (!current) throw new NotFoundError("Opportunity", id);
|
|
2122
2594
|
return current;
|
|
2123
2595
|
}
|
|
2124
|
-
await this.events.
|
|
2596
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.OPPORTUNITY_ASSIGNED, {
|
|
2125
2597
|
opportunityId: id,
|
|
2126
2598
|
ownerId,
|
|
2127
2599
|
...before.ownerId !== void 0 ? { previousOwnerId: before.ownerId } : {}
|
|
@@ -2145,7 +2617,7 @@ var OpportunityRepository = class extends Repository {
|
|
|
2145
2617
|
if (amount === existing.amount) return existing;
|
|
2146
2618
|
const updated = await this.update(id, { amount }, ctx);
|
|
2147
2619
|
if (!updated) throw new NotFoundError("Opportunity", id);
|
|
2148
|
-
await this.events.
|
|
2620
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.OPPORTUNITY_AMOUNT_CHANGED, {
|
|
2149
2621
|
opportunityId: id,
|
|
2150
2622
|
amount,
|
|
2151
2623
|
...existing.amount !== void 0 ? { previousAmount: existing.amount } : {},
|
|
@@ -2172,7 +2644,7 @@ var OpportunityRepository = class extends Repository {
|
|
|
2172
2644
|
if (previous && newDate <= previous) return existing;
|
|
2173
2645
|
const updated = await this.update(id, { expectedCloseAt: newDate }, ctx);
|
|
2174
2646
|
if (!updated) throw new NotFoundError("Opportunity", id);
|
|
2175
|
-
await this.events.
|
|
2647
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.OPPORTUNITY_EXTENDED, {
|
|
2176
2648
|
opportunityId: id,
|
|
2177
2649
|
expectedCloseAt: newDate.toISOString(),
|
|
2178
2650
|
...previous ? { previousExpectedCloseAt: previous.toISOString() } : {},
|
|
@@ -2189,7 +2661,7 @@ var OpportunityRepository = class extends Repository {
|
|
|
2189
2661
|
...by !== void 0 ? { by } : {},
|
|
2190
2662
|
...note !== void 0 ? { note } : {}
|
|
2191
2663
|
}, ctx);
|
|
2192
|
-
await this.events.
|
|
2664
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.OPPORTUNITY_ABANDONED, { opportunityId: id }, ctx));
|
|
2193
2665
|
return updated;
|
|
2194
2666
|
}
|
|
2195
2667
|
async _closeStatus(id, target, patch, extras, ctx) {
|
|
@@ -2286,7 +2758,7 @@ var ContactRepository = class extends Repository {
|
|
|
2286
2758
|
*/
|
|
2287
2759
|
async createContact(input, ctx) {
|
|
2288
2760
|
const created = await this.create(input, ctx);
|
|
2289
|
-
await this.events.
|
|
2761
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.CONTACT_CREATED, {
|
|
2290
2762
|
contactId: String(created._id),
|
|
2291
2763
|
email: created.contact?.email
|
|
2292
2764
|
}, ctx));
|
|
@@ -2301,7 +2773,7 @@ var ContactRepository = class extends Repository {
|
|
|
2301
2773
|
if (!await this.getById(id, ctx)) throw new NotFoundError("Contact", id);
|
|
2302
2774
|
const updated = await this.update(id, patch, ctx);
|
|
2303
2775
|
if (!updated) throw new NotFoundError("Contact", id);
|
|
2304
|
-
await this.events.
|
|
2776
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.CONTACT_ENRICHED, {
|
|
2305
2777
|
contactId: id,
|
|
2306
2778
|
fields: Object.keys(patch),
|
|
2307
2779
|
...extras.provider !== void 0 ? { provider: extras.provider } : {}
|
|
@@ -2322,7 +2794,7 @@ var ContactRepository = class extends Repository {
|
|
|
2322
2794
|
if (!current) throw new NotFoundError("Contact", id);
|
|
2323
2795
|
return current;
|
|
2324
2796
|
}
|
|
2325
|
-
await this.events.
|
|
2797
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.CONTACT_ASSIGNED, {
|
|
2326
2798
|
contactId: id,
|
|
2327
2799
|
ownerId,
|
|
2328
2800
|
...before.ownerId !== void 0 ? { previousOwnerId: before.ownerId } : {}
|
|
@@ -2372,7 +2844,7 @@ var ContactRepository = class extends Repository {
|
|
|
2372
2844
|
const updated = await this.update(primaryId, patch, ctx);
|
|
2373
2845
|
if (!updated) throw new NotFoundError("Contact", primaryId);
|
|
2374
2846
|
await this.delete(secondaryId, ctx);
|
|
2375
|
-
await this.events.
|
|
2847
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.CONTACT_MERGED, {
|
|
2376
2848
|
primaryId,
|
|
2377
2849
|
secondaryId,
|
|
2378
2850
|
strategy
|
|
@@ -2404,7 +2876,7 @@ var AccountRepository = class extends Repository {
|
|
|
2404
2876
|
}
|
|
2405
2877
|
async createAccount(input, ctx) {
|
|
2406
2878
|
const created = await this.create(input, ctx);
|
|
2407
|
-
await this.events.
|
|
2879
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ACCOUNT_CREATED, {
|
|
2408
2880
|
accountId: String(created._id),
|
|
2409
2881
|
name: created.name
|
|
2410
2882
|
}, ctx));
|
|
@@ -2414,7 +2886,7 @@ var AccountRepository = class extends Repository {
|
|
|
2414
2886
|
if (!await this.getById(id, ctx)) throw new NotFoundError("Account", id);
|
|
2415
2887
|
const updated = await this.update(id, patch, ctx);
|
|
2416
2888
|
if (!updated) throw new NotFoundError("Account", id);
|
|
2417
|
-
await this.events.
|
|
2889
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ACCOUNT_ENRICHED, {
|
|
2418
2890
|
accountId: id,
|
|
2419
2891
|
fields: Object.keys(patch),
|
|
2420
2892
|
...extras.provider !== void 0 ? { provider: extras.provider } : {}
|
|
@@ -2434,7 +2906,7 @@ var AccountRepository = class extends Repository {
|
|
|
2434
2906
|
if (!current) throw new NotFoundError("Account", id);
|
|
2435
2907
|
return current;
|
|
2436
2908
|
}
|
|
2437
|
-
await this.events.
|
|
2909
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ACCOUNT_ASSIGNED, {
|
|
2438
2910
|
accountId: id,
|
|
2439
2911
|
ownerId,
|
|
2440
2912
|
...before.ownerId !== void 0 ? { previousOwnerId: before.ownerId } : {}
|
|
@@ -2463,7 +2935,7 @@ var AccountRepository = class extends Repository {
|
|
|
2463
2935
|
const updated = await this.update(primaryId, patch, ctx);
|
|
2464
2936
|
if (!updated) throw new NotFoundError("Account", primaryId);
|
|
2465
2937
|
await this.delete(secondaryId, ctx);
|
|
2466
|
-
await this.events.
|
|
2938
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ACCOUNT_MERGED, {
|
|
2467
2939
|
primaryId,
|
|
2468
2940
|
secondaryId,
|
|
2469
2941
|
strategy
|
|
@@ -2507,7 +2979,7 @@ var CampaignRepository = class extends Repository {
|
|
|
2507
2979
|
if (this.rejectDuplicates && input.name && isDuplicateKeyError(err)) throw new DuplicateCampaignError(input.name);
|
|
2508
2980
|
throw err;
|
|
2509
2981
|
}
|
|
2510
|
-
await this.events.
|
|
2982
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.CAMPAIGN_CREATED, {
|
|
2511
2983
|
campaignId: String(created._id),
|
|
2512
2984
|
name: created.name
|
|
2513
2985
|
}, ctx));
|
|
@@ -2515,7 +2987,7 @@ var CampaignRepository = class extends Repository {
|
|
|
2515
2987
|
}
|
|
2516
2988
|
async activate(id, by, ctx) {
|
|
2517
2989
|
const updated = await this._transition(id, "active", { ...by !== void 0 ? { by } : {} }, ctx);
|
|
2518
|
-
await this.events.
|
|
2990
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.CAMPAIGN_ACTIVATED, { campaignId: id }, ctx));
|
|
2519
2991
|
return updated;
|
|
2520
2992
|
}
|
|
2521
2993
|
async pause(id, reason, by, ctx) {
|
|
@@ -2523,7 +2995,7 @@ var CampaignRepository = class extends Repository {
|
|
|
2523
2995
|
...by !== void 0 ? { by } : {},
|
|
2524
2996
|
...reason !== void 0 ? { note: reason } : {}
|
|
2525
2997
|
}, ctx);
|
|
2526
|
-
await this.events.
|
|
2998
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.CAMPAIGN_PAUSED, {
|
|
2527
2999
|
campaignId: id,
|
|
2528
3000
|
reason
|
|
2529
3001
|
}, ctx));
|
|
@@ -2531,13 +3003,13 @@ var CampaignRepository = class extends Repository {
|
|
|
2531
3003
|
}
|
|
2532
3004
|
async resume(id, by, ctx) {
|
|
2533
3005
|
const updated = await this._transition(id, "active", { ...by !== void 0 ? { by } : {} }, ctx);
|
|
2534
|
-
await this.events.
|
|
3006
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.CAMPAIGN_RESUMED, { campaignId: id }, ctx));
|
|
2535
3007
|
return updated;
|
|
2536
3008
|
}
|
|
2537
3009
|
async complete(id, extras = {}, ctx) {
|
|
2538
3010
|
const patch = extras.actualRevenueAmount !== void 0 ? { actualRevenueAmount: extras.actualRevenueAmount } : {};
|
|
2539
3011
|
const updated = await this._transition(id, "completed", extras, ctx, patch);
|
|
2540
|
-
await this.events.
|
|
3012
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.CAMPAIGN_COMPLETED, {
|
|
2541
3013
|
campaignId: id,
|
|
2542
3014
|
actualRevenueAmount: updated.actualRevenueAmount
|
|
2543
3015
|
}, ctx));
|
|
@@ -2545,7 +3017,7 @@ var CampaignRepository = class extends Repository {
|
|
|
2545
3017
|
}
|
|
2546
3018
|
async archive(id, by, ctx) {
|
|
2547
3019
|
const updated = await this._transition(id, "archived", { ...by !== void 0 ? { by } : {} }, ctx);
|
|
2548
|
-
await this.events.
|
|
3020
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.CAMPAIGN_ARCHIVED, { campaignId: id }, ctx));
|
|
2549
3021
|
return updated;
|
|
2550
3022
|
}
|
|
2551
3023
|
async _transition(id, target, extras, ctx, patch = {}) {
|
|
@@ -2636,7 +3108,7 @@ var SubscriptionRepository = class extends Repository {
|
|
|
2636
3108
|
});
|
|
2637
3109
|
throw err;
|
|
2638
3110
|
}
|
|
2639
|
-
await this.events.
|
|
3111
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.SUBSCRIPTION_CREATED, {
|
|
2640
3112
|
subscriptionId: String(sub._id),
|
|
2641
3113
|
contactId: sub.contactId,
|
|
2642
3114
|
campaignId: sub.campaignId,
|
|
@@ -2649,7 +3121,7 @@ var SubscriptionRepository = class extends Repository {
|
|
|
2649
3121
|
if (!sub || sub.status !== "pending") throw new InvalidConfirmationTokenError();
|
|
2650
3122
|
const confirmedAt = /* @__PURE__ */ new Date();
|
|
2651
3123
|
const updated = await this._transition(String(sub._id), "confirmed", { patch: { confirmedAt } }, ctx);
|
|
2652
|
-
await this.events.
|
|
3124
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.SUBSCRIPTION_CONFIRMED, {
|
|
2653
3125
|
subscriptionId: String(sub._id),
|
|
2654
3126
|
contactId: sub.contactId,
|
|
2655
3127
|
campaignId: sub.campaignId,
|
|
@@ -2669,7 +3141,7 @@ var SubscriptionRepository = class extends Repository {
|
|
|
2669
3141
|
...reason !== void 0 ? { unsubscribeReason: reason } : {}
|
|
2670
3142
|
}
|
|
2671
3143
|
}, ctx);
|
|
2672
|
-
await this.events.
|
|
3144
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.SUBSCRIPTION_UNSUBSCRIBED, {
|
|
2673
3145
|
subscriptionId: String(sub._id),
|
|
2674
3146
|
contactId: sub.contactId,
|
|
2675
3147
|
campaignId: sub.campaignId,
|
|
@@ -2689,7 +3161,7 @@ var SubscriptionRepository = class extends Repository {
|
|
|
2689
3161
|
...reason !== void 0 ? { bounceReason: reason } : {}
|
|
2690
3162
|
}
|
|
2691
3163
|
}, ctx);
|
|
2692
|
-
await this.events.
|
|
3164
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.SUBSCRIPTION_BOUNCED, {
|
|
2693
3165
|
subscriptionId: id,
|
|
2694
3166
|
contactId: existing.contactId,
|
|
2695
3167
|
campaignId: existing.campaignId,
|
|
@@ -2703,7 +3175,7 @@ var SubscriptionRepository = class extends Repository {
|
|
|
2703
3175
|
if (existing.status === "complained") return existing;
|
|
2704
3176
|
const complainedAt = /* @__PURE__ */ new Date();
|
|
2705
3177
|
const updated = await this._transition(id, "complained", { patch: { complainedAt } }, ctx);
|
|
2706
|
-
await this.events.
|
|
3178
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.SUBSCRIPTION_COMPLAINED, {
|
|
2707
3179
|
subscriptionId: id,
|
|
2708
3180
|
contactId: existing.contactId,
|
|
2709
3181
|
campaignId: existing.campaignId
|
|
@@ -2738,6 +3210,78 @@ var SubscriptionRepository = class extends Repository {
|
|
|
2738
3210
|
}
|
|
2739
3211
|
};
|
|
2740
3212
|
//#endregion
|
|
3213
|
+
//#region src/repositories/suppression.repository.ts
|
|
3214
|
+
function normalizeEmail(email) {
|
|
3215
|
+
return email.trim().toLowerCase();
|
|
3216
|
+
}
|
|
3217
|
+
var SuppressionRepository = class extends Repository {
|
|
3218
|
+
events;
|
|
3219
|
+
constructor(model, plugins, deps, pagination) {
|
|
3220
|
+
super(model, plugins, pagination);
|
|
3221
|
+
this.events = deps.events;
|
|
3222
|
+
}
|
|
3223
|
+
/**
|
|
3224
|
+
* Idempotently add `email` to the org's suppression list. Atomic
|
|
3225
|
+
* `getOrCreate` on the unique (org, email) index — of N concurrent
|
|
3226
|
+
* calls exactly one inserts. Emits `crm:suppression.added` only for
|
|
3227
|
+
* the inserting call; an existing row is returned unchanged, no event.
|
|
3228
|
+
*/
|
|
3229
|
+
async suppress(email, input, ctx) {
|
|
3230
|
+
const normalized = normalizeEmail(email);
|
|
3231
|
+
const draft = {
|
|
3232
|
+
email: normalized,
|
|
3233
|
+
reason: input.reason,
|
|
3234
|
+
suppressedAt: /* @__PURE__ */ new Date(),
|
|
3235
|
+
...input.source !== void 0 ? { source: input.source } : {},
|
|
3236
|
+
...input.subjectKind !== void 0 ? { subjectKind: input.subjectKind } : {},
|
|
3237
|
+
...input.subjectId !== void 0 ? { subjectId: input.subjectId } : {},
|
|
3238
|
+
...input.metadata !== void 0 ? { metadata: input.metadata } : {}
|
|
3239
|
+
};
|
|
3240
|
+
const { doc, created } = await this.getOrCreate({ email: normalized }, draft, ctx);
|
|
3241
|
+
if (created) await this.events.dispatch(ctx, createEvent(CRM_EVENTS.SUPPRESSION_ADDED, {
|
|
3242
|
+
email: normalized,
|
|
3243
|
+
reason: input.reason,
|
|
3244
|
+
...input.source !== void 0 ? { source: input.source } : {}
|
|
3245
|
+
}, ctx));
|
|
3246
|
+
return doc;
|
|
3247
|
+
}
|
|
3248
|
+
/** True when `email` is on the org's suppression list. */
|
|
3249
|
+
async isSuppressed(email, ctx) {
|
|
3250
|
+
return await this.exists({ email: normalizeEmail(email) }, ctx) !== null;
|
|
3251
|
+
}
|
|
3252
|
+
/**
|
|
3253
|
+
* Return the suppressed subset of `emails` (normalized) as a Set —
|
|
3254
|
+
* ONE `$in` query, built for the campaign-fanout hot path:
|
|
3255
|
+
*
|
|
3256
|
+
* ```ts
|
|
3257
|
+
* const blocked = await suppression.filterSuppressed(recipients, ctx);
|
|
3258
|
+
* const sendable = recipients.filter((e) => !blocked.has(e.toLowerCase()));
|
|
3259
|
+
* ```
|
|
3260
|
+
*/
|
|
3261
|
+
async filterSuppressed(emails, ctx) {
|
|
3262
|
+
if (emails.length === 0) return /* @__PURE__ */ new Set();
|
|
3263
|
+
const normalized = [...new Set(emails.map(normalizeEmail))];
|
|
3264
|
+
const rows = await this.findAll({ email: { $in: normalized } }, {
|
|
3265
|
+
...ctx ?? {},
|
|
3266
|
+
select: "email",
|
|
3267
|
+
lean: true
|
|
3268
|
+
});
|
|
3269
|
+
return new Set(rows.map((row) => row.email));
|
|
3270
|
+
}
|
|
3271
|
+
/**
|
|
3272
|
+
* Remove `email` from the org's suppression list. Single atomic
|
|
3273
|
+
* `deleteMany` on the (unique) email key — emits
|
|
3274
|
+
* `crm:suppression.removed` only when a row was actually removed.
|
|
3275
|
+
* Returns whether a row was removed.
|
|
3276
|
+
*/
|
|
3277
|
+
async unsuppress(email, ctx) {
|
|
3278
|
+
const normalized = normalizeEmail(email);
|
|
3279
|
+
const removed = (await this.deleteMany({ email: normalized }, ctx ?? {})).deletedCount > 0;
|
|
3280
|
+
if (removed) await this.events.dispatch(ctx, createEvent(CRM_EVENTS.SUPPRESSION_REMOVED, { email: normalized }, ctx));
|
|
3281
|
+
return removed;
|
|
3282
|
+
}
|
|
3283
|
+
};
|
|
3284
|
+
//#endregion
|
|
2741
3285
|
//#region src/repositories/activity.repository.ts
|
|
2742
3286
|
var ActivityRepository = class extends Repository {
|
|
2743
3287
|
events;
|
|
@@ -2751,7 +3295,7 @@ var ActivityRepository = class extends Repository {
|
|
|
2751
3295
|
status: "planned"
|
|
2752
3296
|
};
|
|
2753
3297
|
const created = await this.create(draft, ctx);
|
|
2754
|
-
await this.events.
|
|
3298
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ACTIVITY_PLANNED, {
|
|
2755
3299
|
activityId: String(created._id),
|
|
2756
3300
|
type: created.type,
|
|
2757
3301
|
subjectKind: created.subjectKind,
|
|
@@ -2765,7 +3309,7 @@ var ActivityRepository = class extends Repository {
|
|
|
2765
3309
|
async complete(id, completedAt, ctx) {
|
|
2766
3310
|
const ts = completedAt ?? /* @__PURE__ */ new Date();
|
|
2767
3311
|
const updated = await this._transition(id, "completed", { completedAt: ts }, ctx);
|
|
2768
|
-
await this.events.
|
|
3312
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ACTIVITY_COMPLETED, {
|
|
2769
3313
|
activityId: id,
|
|
2770
3314
|
completedAt: ts.toISOString()
|
|
2771
3315
|
}, ctx));
|
|
@@ -2773,7 +3317,7 @@ var ActivityRepository = class extends Repository {
|
|
|
2773
3317
|
}
|
|
2774
3318
|
async cancel(id, reason, ctx) {
|
|
2775
3319
|
const updated = await this._transition(id, "cancelled", reason ? { metadata: { cancelReason: reason } } : {}, ctx);
|
|
2776
|
-
await this.events.
|
|
3320
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ACTIVITY_CANCELLED, {
|
|
2777
3321
|
activityId: id,
|
|
2778
3322
|
...reason !== void 0 ? { reason } : {}
|
|
2779
3323
|
}, ctx));
|
|
@@ -2795,7 +3339,7 @@ var ActivityRepository = class extends Repository {
|
|
|
2795
3339
|
const previous = existing.dueDate;
|
|
2796
3340
|
const updated = await this.update(id, { dueDate: newDueDate }, ctx);
|
|
2797
3341
|
if (!updated) throw new NotFoundError("Activity", id);
|
|
2798
|
-
await this.events.
|
|
3342
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ACTIVITY_RESCHEDULED, {
|
|
2799
3343
|
activityId: id,
|
|
2800
3344
|
dueDate: newDueDate.toISOString(),
|
|
2801
3345
|
...previous ? { previousDueDate: previous.toISOString() } : {},
|
|
@@ -2822,7 +3366,7 @@ var ActivityRepository = class extends Repository {
|
|
|
2822
3366
|
};
|
|
2823
3367
|
const updated = await this.update(id, { metadata: mergedMetadata }, ctx);
|
|
2824
3368
|
if (!updated) throw new NotFoundError("Activity", id);
|
|
2825
|
-
await this.events.
|
|
3369
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ACTIVITY_OUTCOME_RECORDED, {
|
|
2826
3370
|
activityId: id,
|
|
2827
3371
|
outcome
|
|
2828
3372
|
}, ctx));
|
|
@@ -2879,7 +3423,7 @@ var NoteRepository = class extends Repository {
|
|
|
2879
3423
|
}
|
|
2880
3424
|
async addNote(input, ctx) {
|
|
2881
3425
|
const created = await this.create(input, ctx);
|
|
2882
|
-
await this.events.
|
|
3426
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.NOTE_ADDED, {
|
|
2883
3427
|
noteId: String(created._id),
|
|
2884
3428
|
subjectKind: created.subjectKind,
|
|
2885
3429
|
subjectId: String(created.subjectId)
|
|
@@ -2945,7 +3489,7 @@ var EmailEventRepository = class extends Repository {
|
|
|
2945
3489
|
...input.metadata !== void 0 ? { metadata: input.metadata } : {}
|
|
2946
3490
|
};
|
|
2947
3491
|
const created = await this.create(draft, ctx);
|
|
2948
|
-
await this.events.
|
|
3492
|
+
await this.events.dispatch(ctx, createEvent(KIND_TO_EVENT[kind], {
|
|
2949
3493
|
eventId: String(created._id),
|
|
2950
3494
|
kind,
|
|
2951
3495
|
subjectKind: input.subjectKind,
|
|
@@ -2975,7 +3519,7 @@ var ConversationRepository = class extends Repository {
|
|
|
2975
3519
|
...input.metadata !== void 0 ? { metadata: input.metadata } : {}
|
|
2976
3520
|
};
|
|
2977
3521
|
const created = await this.create(draft, ctx);
|
|
2978
|
-
await this.events.
|
|
3522
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.CONVERSATION_CREATED, {
|
|
2979
3523
|
conversationId: String(created._id),
|
|
2980
3524
|
channel: created.channel,
|
|
2981
3525
|
participantKind: created.participantKind,
|
|
@@ -2996,7 +3540,7 @@ var ConversationRepository = class extends Repository {
|
|
|
2996
3540
|
if (!current) throw new NotFoundError("Conversation", id);
|
|
2997
3541
|
return current;
|
|
2998
3542
|
}
|
|
2999
|
-
await this.events.
|
|
3543
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.CONVERSATION_ASSIGNED, {
|
|
3000
3544
|
conversationId: id,
|
|
3001
3545
|
assigneeId,
|
|
3002
3546
|
...before.assigneeId !== void 0 ? { previousAssigneeId: before.assigneeId } : {}
|
|
@@ -3034,7 +3578,7 @@ var ConversationRepository = class extends Repository {
|
|
|
3034
3578
|
if (existing.status === next) return existing;
|
|
3035
3579
|
const updated = await this.update(id, { status: next }, ctx);
|
|
3036
3580
|
if (!updated) throw new NotFoundError("Conversation", id);
|
|
3037
|
-
await this.events.
|
|
3581
|
+
await this.events.dispatch(ctx, createEvent(eventType, {
|
|
3038
3582
|
conversationId: id,
|
|
3039
3583
|
status: next,
|
|
3040
3584
|
previousStatus: existing.status
|
|
@@ -3086,7 +3630,7 @@ var MessageRepository = class extends Repository {
|
|
|
3086
3630
|
direction,
|
|
3087
3631
|
sentAt
|
|
3088
3632
|
}, ctx);
|
|
3089
|
-
await this.events.
|
|
3633
|
+
await this.events.dispatch(ctx, createEvent(eventType, {
|
|
3090
3634
|
messageId: String(created._id),
|
|
3091
3635
|
conversationId: input.conversationId,
|
|
3092
3636
|
direction,
|
|
@@ -3111,6 +3655,230 @@ var MessageRepository = class extends Repository {
|
|
|
3111
3655
|
}
|
|
3112
3656
|
};
|
|
3113
3657
|
//#endregion
|
|
3658
|
+
//#region src/repositories/sequence.repository.ts
|
|
3659
|
+
var SequenceRepository = class extends Repository {
|
|
3660
|
+
events;
|
|
3661
|
+
constructor(model, plugins, deps, pagination) {
|
|
3662
|
+
super(model, plugins, pagination);
|
|
3663
|
+
this.events = deps.events;
|
|
3664
|
+
}
|
|
3665
|
+
async activate(id, ctx) {
|
|
3666
|
+
const doc = await this._transition(id, "active", ctx);
|
|
3667
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.SEQUENCE_ACTIVATED, { sequenceId: id }, ctx));
|
|
3668
|
+
return doc;
|
|
3669
|
+
}
|
|
3670
|
+
async activateFromPaused(id, ctx) {
|
|
3671
|
+
return this.activate(id, ctx);
|
|
3672
|
+
}
|
|
3673
|
+
async pause(id, ctx) {
|
|
3674
|
+
const doc = await this._transition(id, "paused", ctx);
|
|
3675
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.SEQUENCE_PAUSED, { sequenceId: id }, ctx));
|
|
3676
|
+
return doc;
|
|
3677
|
+
}
|
|
3678
|
+
async archive(id, ctx) {
|
|
3679
|
+
const doc = await this._transition(id, "archived", ctx);
|
|
3680
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.SEQUENCE_ARCHIVED, { sequenceId: id }, ctx));
|
|
3681
|
+
return doc;
|
|
3682
|
+
}
|
|
3683
|
+
async _transition(id, target, ctx) {
|
|
3684
|
+
const existing = await this.getById(id, ctx);
|
|
3685
|
+
if (!existing) throw new NotFoundError("Sequence", id);
|
|
3686
|
+
SEQUENCE_MACHINE.assertTransition(id, existing.status, target);
|
|
3687
|
+
const entry = {
|
|
3688
|
+
status: target,
|
|
3689
|
+
occurredAt: /* @__PURE__ */ new Date(),
|
|
3690
|
+
...ctx?.actorId !== void 0 ? { by: String(ctx.actorId) } : {}
|
|
3691
|
+
};
|
|
3692
|
+
const updated = await assertAndClaim(SEQUENCE_MACHINE, this, id, {
|
|
3693
|
+
from: existing.status,
|
|
3694
|
+
to: target,
|
|
3695
|
+
patch: { statusHistory: [...existing.statusHistory ?? [], entry] },
|
|
3696
|
+
options: ctx
|
|
3697
|
+
});
|
|
3698
|
+
if (!updated) throw new InvalidSequenceTransitionError({
|
|
3699
|
+
from: (await this.getById(id, ctx))?.status ?? existing.status,
|
|
3700
|
+
to: target,
|
|
3701
|
+
sequenceId: id
|
|
3702
|
+
});
|
|
3703
|
+
return updated;
|
|
3704
|
+
}
|
|
3705
|
+
};
|
|
3706
|
+
//#endregion
|
|
3707
|
+
//#region src/repositories/enrollment.repository.ts
|
|
3708
|
+
var EnrollmentRepository = class extends Repository {
|
|
3709
|
+
events;
|
|
3710
|
+
sequences;
|
|
3711
|
+
constructor(model, plugins, deps, pagination) {
|
|
3712
|
+
super(model, plugins, pagination);
|
|
3713
|
+
this.events = deps.events;
|
|
3714
|
+
this.sequences = deps.sequences;
|
|
3715
|
+
}
|
|
3716
|
+
/** Create a new enrollment — throws AlreadyEnrolledError if an active one exists. */
|
|
3717
|
+
async enroll(input, ctx) {
|
|
3718
|
+
const { sequenceId, subjectKind, subjectId, workflowRunId, ownerId, metadata } = input;
|
|
3719
|
+
if (await this.getOne({
|
|
3720
|
+
sequenceId,
|
|
3721
|
+
subjectId,
|
|
3722
|
+
status: { $in: ["active", "paused"] }
|
|
3723
|
+
}, ctx)) throw new AlreadyEnrolledError({
|
|
3724
|
+
sequenceId,
|
|
3725
|
+
subjectId
|
|
3726
|
+
});
|
|
3727
|
+
const doc = await this.create({
|
|
3728
|
+
organizationId: ctx?.organizationId,
|
|
3729
|
+
sequenceId,
|
|
3730
|
+
subjectKind,
|
|
3731
|
+
subjectId,
|
|
3732
|
+
status: "active",
|
|
3733
|
+
currentStepIndex: 0,
|
|
3734
|
+
stepResults: [],
|
|
3735
|
+
enrolledAt: /* @__PURE__ */ new Date(),
|
|
3736
|
+
...workflowRunId ? { workflowRunId } : {},
|
|
3737
|
+
...ownerId ? { ownerId } : {},
|
|
3738
|
+
...metadata ? { metadata } : {},
|
|
3739
|
+
statusHistory: [{
|
|
3740
|
+
status: "active",
|
|
3741
|
+
occurredAt: /* @__PURE__ */ new Date()
|
|
3742
|
+
}]
|
|
3743
|
+
}, ctx);
|
|
3744
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ENROLLMENT_CREATED, {
|
|
3745
|
+
enrollmentId: String(doc._id),
|
|
3746
|
+
sequenceId,
|
|
3747
|
+
subjectId,
|
|
3748
|
+
subjectKind
|
|
3749
|
+
}, ctx));
|
|
3750
|
+
return doc;
|
|
3751
|
+
}
|
|
3752
|
+
/** Set workflowRunId after the workflow has started. */
|
|
3753
|
+
async setWorkflowRunId(id, workflowRunId, ctx) {
|
|
3754
|
+
await this.update(id, { workflowRunId }, ctx);
|
|
3755
|
+
}
|
|
3756
|
+
/** Atomically advance to the next step and record the result (CAS). */
|
|
3757
|
+
async advanceStep(id, nextStepIndex, stepResult, ctx) {
|
|
3758
|
+
return this.claim(id, {
|
|
3759
|
+
field: "currentStepIndex",
|
|
3760
|
+
from: nextStepIndex - 1,
|
|
3761
|
+
to: nextStepIndex,
|
|
3762
|
+
where: { status: "active" }
|
|
3763
|
+
}, stepResult ? { $push: { stepResults: stepResult } } : {}, ctx);
|
|
3764
|
+
}
|
|
3765
|
+
async pause(id, ctx) {
|
|
3766
|
+
const doc = await this._transition(id, "paused", ctx);
|
|
3767
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ENROLLMENT_PAUSED, { enrollmentId: id }, ctx));
|
|
3768
|
+
return doc;
|
|
3769
|
+
}
|
|
3770
|
+
async resume(id, ctx) {
|
|
3771
|
+
const doc = await this._transition(id, "active", ctx);
|
|
3772
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ENROLLMENT_RESUMED, { enrollmentId: id }, ctx));
|
|
3773
|
+
return doc;
|
|
3774
|
+
}
|
|
3775
|
+
async complete(id, ctx) {
|
|
3776
|
+
const doc = await this._transition(id, "completed", ctx, { completedAt: /* @__PURE__ */ new Date() });
|
|
3777
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ENROLLMENT_COMPLETED, { enrollmentId: id }, ctx));
|
|
3778
|
+
return doc;
|
|
3779
|
+
}
|
|
3780
|
+
async exit(id, exitReason, ctx) {
|
|
3781
|
+
const existing = await this.getById(id, ctx);
|
|
3782
|
+
if (!existing) throw new NotFoundError("Enrollment", id);
|
|
3783
|
+
const from = existing.status;
|
|
3784
|
+
ENROLLMENT_MACHINE.assertTransition(id, from, "exited");
|
|
3785
|
+
const entry = {
|
|
3786
|
+
status: "exited",
|
|
3787
|
+
occurredAt: /* @__PURE__ */ new Date(),
|
|
3788
|
+
...ctx?.actorId ? { by: String(ctx.actorId) } : {}
|
|
3789
|
+
};
|
|
3790
|
+
const updated = await assertAndClaim(ENROLLMENT_MACHINE, this, id, {
|
|
3791
|
+
from,
|
|
3792
|
+
to: "exited",
|
|
3793
|
+
patch: {
|
|
3794
|
+
exitReason,
|
|
3795
|
+
statusHistory: [...existing.statusHistory ?? [], entry]
|
|
3796
|
+
},
|
|
3797
|
+
options: ctx
|
|
3798
|
+
});
|
|
3799
|
+
if (!updated) throw new NotFoundError("Enrollment", id);
|
|
3800
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.ENROLLMENT_EXITED, {
|
|
3801
|
+
enrollmentId: id,
|
|
3802
|
+
exitReason
|
|
3803
|
+
}, ctx));
|
|
3804
|
+
return updated;
|
|
3805
|
+
}
|
|
3806
|
+
/**
|
|
3807
|
+
* Reply detection: exit every ACTIVE enrollment of the given subjects
|
|
3808
|
+
* with reason `'reply'` — the one method a host's
|
|
3809
|
+
* `crm:message.received` subscriber calls:
|
|
3810
|
+
*
|
|
3811
|
+
* ```ts
|
|
3812
|
+
* crm.events.subscribe('crm:message.received', async (e) => {
|
|
3813
|
+
* await crm.repositories.enrollment.exitActiveOnReply(
|
|
3814
|
+
* [{ subjectKind: 'contact', subjectId: e.payload.participantId }],
|
|
3815
|
+
* { organizationId: e.meta.organizationId },
|
|
3816
|
+
* );
|
|
3817
|
+
* });
|
|
3818
|
+
* ```
|
|
3819
|
+
*
|
|
3820
|
+
* Sequences are batch-loaded in ONE `$in` query; enrollments whose
|
|
3821
|
+
* sequence has `replyDetectionEnabled === false` are skipped (a missing
|
|
3822
|
+
* sequence — e.g. deleted — does NOT skip: the default is to exit).
|
|
3823
|
+
* Each exit goes through `exit()` (state-machine CAS + statusHistory +
|
|
3824
|
+
* ENROLLMENT_EXITED event). An enrollment that a concurrent transition
|
|
3825
|
+
* already moved out of `active` loses the CAS and is skipped — the
|
|
3826
|
+
* desired end state ("not active") already holds.
|
|
3827
|
+
*
|
|
3828
|
+
* Returns the enrollments this call actually exited.
|
|
3829
|
+
*/
|
|
3830
|
+
async exitActiveOnReply(subjects, ctx) {
|
|
3831
|
+
if (subjects.length === 0) return [];
|
|
3832
|
+
const active = await this.findAll({
|
|
3833
|
+
status: "active",
|
|
3834
|
+
$or: subjects.map((s) => ({
|
|
3835
|
+
subjectKind: s.subjectKind,
|
|
3836
|
+
subjectId: s.subjectId
|
|
3837
|
+
}))
|
|
3838
|
+
}, ctx);
|
|
3839
|
+
if (active.length === 0) return [];
|
|
3840
|
+
const sequenceIds = [...new Set(active.map((e) => String(e.sequenceId)))];
|
|
3841
|
+
const sequences = await this.sequences.findAll({ _id: { $in: sequenceIds } }, ctx);
|
|
3842
|
+
const sequencesById = new Map(sequences.map((s) => [String(s._id), s]));
|
|
3843
|
+
const exited = [];
|
|
3844
|
+
for (const enrollment of active) {
|
|
3845
|
+
if (sequencesById.get(String(enrollment.sequenceId))?.replyDetectionEnabled === false) continue;
|
|
3846
|
+
try {
|
|
3847
|
+
exited.push(await this.exit(String(enrollment._id), "reply", ctx));
|
|
3848
|
+
} catch (err) {
|
|
3849
|
+
if (err instanceof IllegalTransitionError || err instanceof InvalidEnrollmentTransitionError || err instanceof NotFoundError) continue;
|
|
3850
|
+
throw err;
|
|
3851
|
+
}
|
|
3852
|
+
}
|
|
3853
|
+
return exited;
|
|
3854
|
+
}
|
|
3855
|
+
async _transition(id, target, ctx, patch = {}) {
|
|
3856
|
+
const existing = await this.getById(id, ctx);
|
|
3857
|
+
if (!existing) throw new NotFoundError("Enrollment", id);
|
|
3858
|
+
ENROLLMENT_MACHINE.assertTransition(id, existing.status, target);
|
|
3859
|
+
const entry = {
|
|
3860
|
+
status: target,
|
|
3861
|
+
occurredAt: /* @__PURE__ */ new Date(),
|
|
3862
|
+
...ctx?.actorId ? { by: String(ctx.actorId) } : {}
|
|
3863
|
+
};
|
|
3864
|
+
const updated = await assertAndClaim(ENROLLMENT_MACHINE, this, id, {
|
|
3865
|
+
from: existing.status,
|
|
3866
|
+
to: target,
|
|
3867
|
+
patch: {
|
|
3868
|
+
...patch,
|
|
3869
|
+
statusHistory: [...existing.statusHistory ?? [], entry]
|
|
3870
|
+
},
|
|
3871
|
+
options: ctx
|
|
3872
|
+
});
|
|
3873
|
+
if (!updated) throw new InvalidEnrollmentTransitionError({
|
|
3874
|
+
from: (await this.getById(id, ctx))?.status ?? existing.status,
|
|
3875
|
+
to: target,
|
|
3876
|
+
enrollmentId: id
|
|
3877
|
+
});
|
|
3878
|
+
return updated;
|
|
3879
|
+
}
|
|
3880
|
+
};
|
|
3881
|
+
//#endregion
|
|
3114
3882
|
//#region src/repositories/create-repositories.ts
|
|
3115
3883
|
/**
|
|
3116
3884
|
* Repository factory — composes the shared mongokit plugin stack and
|
|
@@ -3144,6 +3912,7 @@ function createCrmRepositories(models, deps) {
|
|
|
3144
3912
|
const account = new AccountRepository(models.Account, plugins, { events }, pagination);
|
|
3145
3913
|
const campaign = new CampaignRepository(models.Campaign, plugins, { events }, pagination);
|
|
3146
3914
|
const subscription = new SubscriptionRepository(models.Subscription, plugins, { events }, pagination);
|
|
3915
|
+
const suppression = new SuppressionRepository(models.Suppression, plugins, { events }, pagination);
|
|
3147
3916
|
const activity = new ActivityRepository(models.Activity, plugins, { events }, pagination);
|
|
3148
3917
|
const note = new NoteRepository(models.Note, plugins, { events }, pagination);
|
|
3149
3918
|
const lossReason = new LossReasonRepository(models.LossReason, plugins, { events }, pagination);
|
|
@@ -3153,6 +3922,11 @@ function createCrmRepositories(models, deps) {
|
|
|
3153
3922
|
events,
|
|
3154
3923
|
conversations: conversation
|
|
3155
3924
|
}, pagination);
|
|
3925
|
+
const sequence = new SequenceRepository(models.Sequence, plugins, { events }, pagination);
|
|
3926
|
+
const enrollment = new EnrollmentRepository(models.Enrollment, plugins, {
|
|
3927
|
+
events,
|
|
3928
|
+
sequences: sequence
|
|
3929
|
+
}, pagination);
|
|
3156
3930
|
return Object.freeze({
|
|
3157
3931
|
pipeline,
|
|
3158
3932
|
lead,
|
|
@@ -3161,15 +3935,127 @@ function createCrmRepositories(models, deps) {
|
|
|
3161
3935
|
account,
|
|
3162
3936
|
campaign,
|
|
3163
3937
|
subscription,
|
|
3938
|
+
suppression,
|
|
3164
3939
|
activity,
|
|
3165
3940
|
note,
|
|
3166
3941
|
lossReason,
|
|
3167
3942
|
emailEvent,
|
|
3168
3943
|
conversation,
|
|
3169
|
-
message
|
|
3944
|
+
message,
|
|
3945
|
+
sequence,
|
|
3946
|
+
enrollment
|
|
3170
3947
|
});
|
|
3171
3948
|
}
|
|
3172
3949
|
//#endregion
|
|
3950
|
+
//#region src/events/dispatch.ts
|
|
3951
|
+
/**
|
|
3952
|
+
* Context key for the post-commit event queue. `Symbol.for` so the host
|
|
3953
|
+
* and the package agree on the key even across duplicated installs.
|
|
3954
|
+
*/
|
|
3955
|
+
const PENDING_EVENTS = Symbol.for("@classytic/crm:pendingEvents");
|
|
3956
|
+
/**
|
|
3957
|
+
* EventTransport-compatible wrapper the engine hands to repositories.
|
|
3958
|
+
* `publish`/`subscribe` delegate straight to the underlying transport
|
|
3959
|
+
* (host-facing surface unchanged); domain verbs call `dispatch(ctx, event)`
|
|
3960
|
+
* so session/outbox/queue routing happens in ONE place.
|
|
3961
|
+
*/
|
|
3962
|
+
var CrmEventDispatcher = class {
|
|
3963
|
+
transport;
|
|
3964
|
+
outbox;
|
|
3965
|
+
logger;
|
|
3966
|
+
constructor(options) {
|
|
3967
|
+
this.transport = options.transport;
|
|
3968
|
+
this.outbox = options.outbox;
|
|
3969
|
+
this.logger = options.logger;
|
|
3970
|
+
}
|
|
3971
|
+
get name() {
|
|
3972
|
+
return this.transport.name;
|
|
3973
|
+
}
|
|
3974
|
+
/** Raw immediate publish — post-commit flushes and host-side use only. */
|
|
3975
|
+
publish(event) {
|
|
3976
|
+
return this.transport.publish(event);
|
|
3977
|
+
}
|
|
3978
|
+
subscribe(pattern, handler) {
|
|
3979
|
+
return this.transport.subscribe(pattern, handler);
|
|
3980
|
+
}
|
|
3981
|
+
async close() {
|
|
3982
|
+
await this.transport.close?.();
|
|
3983
|
+
}
|
|
3984
|
+
/**
|
|
3985
|
+
* Session-bound dispatch (see module doc for the full contract).
|
|
3986
|
+
* `ctx` comes first so call sites read `dispatch(ctx, createEvent(...))`.
|
|
3987
|
+
*/
|
|
3988
|
+
async dispatch(ctx, event) {
|
|
3989
|
+
const pending = ctx?.[PENDING_EVENTS];
|
|
3990
|
+
const session = ctx?.session;
|
|
3991
|
+
if (this.outbox) {
|
|
3992
|
+
const saveOptions = {};
|
|
3993
|
+
if (session !== void 0) saveOptions.session = session;
|
|
3994
|
+
await this.outbox.save(event, saveOptions);
|
|
3995
|
+
} else if (session && !pending) throw new UnmanagedSessionError(event.type);
|
|
3996
|
+
if (pending) {
|
|
3997
|
+
pending.push(event);
|
|
3998
|
+
return;
|
|
3999
|
+
}
|
|
4000
|
+
if (!session) try {
|
|
4001
|
+
await this.transport.publish(event);
|
|
4002
|
+
} catch (err) {
|
|
4003
|
+
this.logger?.error?.(`[crm] publish failed for ${event.type}`, { err });
|
|
4004
|
+
}
|
|
4005
|
+
}
|
|
4006
|
+
};
|
|
4007
|
+
/**
|
|
4008
|
+
* Drain a `[PENDING_EVENTS]` queue through a transport after commit,
|
|
4009
|
+
* swallowing per-event errors (durable delivery is the outbox's job).
|
|
4010
|
+
* Hosts composing their own outer transaction call this after commit;
|
|
4011
|
+
* most should use `engine.withTransaction(ctx, fn)` instead.
|
|
4012
|
+
*/
|
|
4013
|
+
async function flushPending(transport, events, logger) {
|
|
4014
|
+
if (!transport || events.length === 0) return;
|
|
4015
|
+
for (const event of events) try {
|
|
4016
|
+
await transport.publish(event);
|
|
4017
|
+
} catch (err) {
|
|
4018
|
+
(logger?.error ?? console.error)(`[crm] publish failed for ${event.type}`, { err });
|
|
4019
|
+
}
|
|
4020
|
+
}
|
|
4021
|
+
//#endregion
|
|
4022
|
+
//#region src/events/in-process-bus.ts
|
|
4023
|
+
/**
|
|
4024
|
+
* In-process event bus — default fallback when no `eventTransport` is
|
|
4025
|
+
* provided to `createCrm`. Structurally identical to arc's
|
|
4026
|
+
* `MemoryEventTransport`; pattern matching is delegated to primitives.
|
|
4027
|
+
* Handler errors are swallowed — event emission never blocks writes.
|
|
4028
|
+
*/
|
|
4029
|
+
var InProcessCrmBus = class {
|
|
4030
|
+
name = "in-process-crm";
|
|
4031
|
+
subs = [];
|
|
4032
|
+
logger;
|
|
4033
|
+
constructor(options) {
|
|
4034
|
+
if (options?.logger) this.logger = options.logger;
|
|
4035
|
+
}
|
|
4036
|
+
async publish(event) {
|
|
4037
|
+
const matching = this.subs.filter((s) => matchEventPattern(s.pattern, event.type));
|
|
4038
|
+
for (const sub of matching) try {
|
|
4039
|
+
await sub.handler(event);
|
|
4040
|
+
} catch (err) {
|
|
4041
|
+
this.logger?.error?.(`[crm] event handler error [${event.type}]`, { error: err instanceof Error ? err.message : String(err) });
|
|
4042
|
+
}
|
|
4043
|
+
}
|
|
4044
|
+
async subscribe(pattern, handler) {
|
|
4045
|
+
const sub = {
|
|
4046
|
+
pattern,
|
|
4047
|
+
handler
|
|
4048
|
+
};
|
|
4049
|
+
this.subs.push(sub);
|
|
4050
|
+
return () => {
|
|
4051
|
+
this.subs = this.subs.filter((s) => s !== sub);
|
|
4052
|
+
};
|
|
4053
|
+
}
|
|
4054
|
+
async close() {
|
|
4055
|
+
this.subs = [];
|
|
4056
|
+
}
|
|
4057
|
+
};
|
|
4058
|
+
//#endregion
|
|
3173
4059
|
//#region src/engine/ensure-ready.ts
|
|
3174
4060
|
const REQUIRED_CAPABILITIES = [
|
|
3175
4061
|
"upsert",
|
|
@@ -3188,6 +4074,42 @@ function assertCrmCapabilities(probe, options = {}) {
|
|
|
3188
4074
|
}
|
|
3189
4075
|
}
|
|
3190
4076
|
//#endregion
|
|
4077
|
+
//#region src/engine/run-unit.ts
|
|
4078
|
+
/**
|
|
4079
|
+
* runUnit — transaction + post-commit-publish discipline for CRM verbs
|
|
4080
|
+
* composed into one atomic unit (the canonical case: `LeadService.convert`
|
|
4081
|
+
* = Account + Contact + Opportunity + lead transition). Same contract as
|
|
4082
|
+
* `@classytic/refit` / `@classytic/review` / `@classytic/cart` /
|
|
4083
|
+
* `@classytic/invoice` (PACKAGE_RULES §P8.1):
|
|
4084
|
+
*
|
|
4085
|
+
* 1. Events dispatched inside the unit are QUEUED (`ctx[PENDING_EVENTS]`)
|
|
4086
|
+
* and flushed only after the transaction commits (queue resets on
|
|
4087
|
+
* retry attempts) — a rollback can't leak ghost events to in-process
|
|
4088
|
+
* subscribers. Outbox rows stay inside the transaction — the relay
|
|
4089
|
+
* reads only committed rows.
|
|
4090
|
+
* 2. A caller-owned `ctx.session` must arrive WITH a queue or an outbox —
|
|
4091
|
+
* `dispatch()` throws `UnmanagedSessionError` otherwise.
|
|
4092
|
+
*/
|
|
4093
|
+
async function runUnit(config, ctx, body) {
|
|
4094
|
+
const internalCtx = ctx;
|
|
4095
|
+
const externalQueue = internalCtx[PENDING_EVENTS];
|
|
4096
|
+
if (ctx.session) return body(externalQueue ? {
|
|
4097
|
+
...internalCtx,
|
|
4098
|
+
[PENDING_EVENTS]: externalQueue
|
|
4099
|
+
} : internalCtx);
|
|
4100
|
+
const pending = [];
|
|
4101
|
+
const result = await withTransaction(config.connection, async (session) => {
|
|
4102
|
+
pending.length = 0;
|
|
4103
|
+
return body({
|
|
4104
|
+
...internalCtx,
|
|
4105
|
+
session,
|
|
4106
|
+
[PENDING_EVENTS]: pending
|
|
4107
|
+
});
|
|
4108
|
+
}, config.allowNonTransactional ? { allowFallback: true } : {});
|
|
4109
|
+
await flushPending(config.events, pending, config.logger);
|
|
4110
|
+
return result;
|
|
4111
|
+
}
|
|
4112
|
+
//#endregion
|
|
3191
4113
|
//#region src/engine/create-crm.ts
|
|
3192
4114
|
async function createCrm(config) {
|
|
3193
4115
|
if (!config.connection) throw new Error("[crm] CrmConfig.connection is required");
|
|
@@ -3201,7 +4123,11 @@ async function createCrm(config) {
|
|
|
3201
4123
|
...config.pagination !== void 0 ? { pagination: config.pagination } : {},
|
|
3202
4124
|
plugins: config.plugins ?? []
|
|
3203
4125
|
};
|
|
3204
|
-
const events =
|
|
4126
|
+
const events = new CrmEventDispatcher({
|
|
4127
|
+
transport: config.eventTransport ?? new InProcessCrmBus(config.logger ? { logger: config.logger } : void 0),
|
|
4128
|
+
outbox: config.outbox,
|
|
4129
|
+
logger: config.logger
|
|
4130
|
+
});
|
|
3205
4131
|
const models = createCrmModels(config.connection, resolved);
|
|
3206
4132
|
const repositories = createCrmRepositories(models, {
|
|
3207
4133
|
events,
|
|
@@ -3216,11 +4142,19 @@ async function createCrm(config) {
|
|
|
3216
4142
|
repositories,
|
|
3217
4143
|
events,
|
|
3218
4144
|
config: Object.freeze(resolved),
|
|
4145
|
+
withTransaction(ctx, body) {
|
|
4146
|
+
return runUnit({
|
|
4147
|
+
connection: config.connection,
|
|
4148
|
+
events,
|
|
4149
|
+
logger: config.logger,
|
|
4150
|
+
allowNonTransactional: config.allowNonTransactional ?? false
|
|
4151
|
+
}, ctx, body);
|
|
4152
|
+
},
|
|
3219
4153
|
async syncIndexes() {
|
|
3220
4154
|
await Promise.all(Object.values(models).map((m) => m.syncIndexes()));
|
|
3221
4155
|
},
|
|
3222
4156
|
async dispose() {
|
|
3223
|
-
await events.close
|
|
4157
|
+
await events.close();
|
|
3224
4158
|
}
|
|
3225
4159
|
});
|
|
3226
4160
|
}
|
|
@@ -3312,7 +4246,7 @@ var LeadService = class LeadService {
|
|
|
3312
4246
|
opportunityId: String(opportunity._id),
|
|
3313
4247
|
...input.by !== void 0 ? { by: input.by } : {}
|
|
3314
4248
|
}, ctx);
|
|
3315
|
-
await this.events.
|
|
4249
|
+
await this.events.dispatch(ctx, createEvent(CRM_EVENTS.LEAD_CONVERTED, {
|
|
3316
4250
|
leadId,
|
|
3317
4251
|
contactId: String(contact._id),
|
|
3318
4252
|
accountId: account ? String(account._id) : void 0,
|
|
@@ -3400,14 +4334,721 @@ function splitLeadName(lead) {
|
|
|
3400
4334
|
};
|
|
3401
4335
|
}
|
|
3402
4336
|
//#endregion
|
|
4337
|
+
//#region src/events/event-catalog.ts
|
|
4338
|
+
/**
|
|
4339
|
+
* Zod-source event catalog (PACKAGE_RULES rule 18).
|
|
4340
|
+
*
|
|
4341
|
+
* One Zod schema per event is the source of truth — runtime validation,
|
|
4342
|
+
* static types, and Arc introspection all derive from it. Hosts wire the
|
|
4343
|
+
* definitions into arc's `EventRegistry`:
|
|
4344
|
+
*
|
|
4345
|
+
* ```ts
|
|
4346
|
+
* for (const def of crmEventDefinitions) registry.register(def);
|
|
4347
|
+
* ```
|
|
4348
|
+
*
|
|
4349
|
+
* which lights up publish-time validation (`eventPlugin({ registry,
|
|
4350
|
+
* validateMode: 'reject' })`), OpenAPI event introspection, and
|
|
4351
|
+
* `arc describe` output — with zero arc imports here (the definition shape
|
|
4352
|
+
* is structurally identical to arc's `EventDefinitionOutput`).
|
|
4353
|
+
*
|
|
4354
|
+
* Timestamps in payloads are `z.iso.datetime()` (NEVER `z.coerce.date()`):
|
|
4355
|
+
* `z.toJSONSchema()` throws on coerce.date, and events are ISO strings on
|
|
4356
|
+
* the wire (rule 25).
|
|
4357
|
+
*/
|
|
4358
|
+
function defineCrmEvent(input) {
|
|
4359
|
+
const { name, resource, description, version = 1, zodSchema } = input;
|
|
4360
|
+
return {
|
|
4361
|
+
name,
|
|
4362
|
+
version,
|
|
4363
|
+
description,
|
|
4364
|
+
zodSchema,
|
|
4365
|
+
schema: z.toJSONSchema(zodSchema),
|
|
4366
|
+
create(payload, meta) {
|
|
4367
|
+
return createEvent$1(name, payload, {
|
|
4368
|
+
resource,
|
|
4369
|
+
source: "crm",
|
|
4370
|
+
...meta
|
|
4371
|
+
});
|
|
4372
|
+
}
|
|
4373
|
+
};
|
|
4374
|
+
}
|
|
4375
|
+
const ownerChangeSchema = z.object({
|
|
4376
|
+
ownerId: z.string(),
|
|
4377
|
+
previousOwnerId: z.string().optional()
|
|
4378
|
+
});
|
|
4379
|
+
const enrichSchema = z.object({
|
|
4380
|
+
fields: z.array(z.string()),
|
|
4381
|
+
provider: z.string().optional()
|
|
4382
|
+
});
|
|
4383
|
+
const mergeSchema = z.object({
|
|
4384
|
+
primaryId: z.string(),
|
|
4385
|
+
secondaryId: z.string(),
|
|
4386
|
+
strategy: z.enum(["preferPrimary", "preferSecondary"])
|
|
4387
|
+
});
|
|
4388
|
+
const stageRefSchema = z.object({
|
|
4389
|
+
stageId: z.string(),
|
|
4390
|
+
sequence: z.number()
|
|
4391
|
+
});
|
|
4392
|
+
const LeadCreated = defineCrmEvent({
|
|
4393
|
+
name: CRM_EVENTS.LEAD_CREATED,
|
|
4394
|
+
resource: "lead",
|
|
4395
|
+
description: "createLead() inserted a scored lead.",
|
|
4396
|
+
zodSchema: z.object({
|
|
4397
|
+
leadId: z.string(),
|
|
4398
|
+
email: z.string().optional(),
|
|
4399
|
+
score: z.number()
|
|
4400
|
+
})
|
|
4401
|
+
});
|
|
4402
|
+
const LeadStatusChanged = defineCrmEvent({
|
|
4403
|
+
name: CRM_EVENTS.LEAD_STATUS_CHANGED,
|
|
4404
|
+
resource: "lead",
|
|
4405
|
+
description: "A lead lifecycle transition committed (qualify/disqualify/nurture/...).",
|
|
4406
|
+
zodSchema: z.object({
|
|
4407
|
+
leadId: z.string(),
|
|
4408
|
+
from: z.string(),
|
|
4409
|
+
to: z.string()
|
|
4410
|
+
})
|
|
4411
|
+
});
|
|
4412
|
+
const LeadScored = defineCrmEvent({
|
|
4413
|
+
name: CRM_EVENTS.LEAD_SCORED,
|
|
4414
|
+
resource: "lead",
|
|
4415
|
+
description: "A lead score was (re)computed and persisted.",
|
|
4416
|
+
zodSchema: z.object({
|
|
4417
|
+
leadId: z.string(),
|
|
4418
|
+
score: z.number()
|
|
4419
|
+
})
|
|
4420
|
+
});
|
|
4421
|
+
const LeadConverted = defineCrmEvent({
|
|
4422
|
+
name: CRM_EVENTS.LEAD_CONVERTED,
|
|
4423
|
+
resource: "lead",
|
|
4424
|
+
description: "convert() spawned Contact (+ Account) + Opportunity and closed the lead.",
|
|
4425
|
+
zodSchema: z.object({
|
|
4426
|
+
leadId: z.string(),
|
|
4427
|
+
contactId: z.string(),
|
|
4428
|
+
accountId: z.string().optional(),
|
|
4429
|
+
opportunityId: z.string()
|
|
4430
|
+
})
|
|
4431
|
+
});
|
|
4432
|
+
const LeadDisqualified = defineCrmEvent({
|
|
4433
|
+
name: CRM_EVENTS.LEAD_DISQUALIFIED,
|
|
4434
|
+
resource: "lead",
|
|
4435
|
+
description: "disqualify() rejected a lead with a reason.",
|
|
4436
|
+
zodSchema: z.object({
|
|
4437
|
+
leadId: z.string(),
|
|
4438
|
+
reason: z.string()
|
|
4439
|
+
})
|
|
4440
|
+
});
|
|
4441
|
+
const LeadEnriched = defineCrmEvent({
|
|
4442
|
+
name: CRM_EVENTS.LEAD_ENRICHED,
|
|
4443
|
+
resource: "lead",
|
|
4444
|
+
description: "enrich() merged a provider/manual patch into the lead.",
|
|
4445
|
+
zodSchema: z.object({ leadId: z.string() }).extend(enrichSchema.shape)
|
|
4446
|
+
});
|
|
4447
|
+
const LeadAssigned = defineCrmEvent({
|
|
4448
|
+
name: CRM_EVENTS.LEAD_ASSIGNED,
|
|
4449
|
+
resource: "lead",
|
|
4450
|
+
description: "assign() (re)assigned the lead owner.",
|
|
4451
|
+
zodSchema: z.object({ leadId: z.string() }).extend(ownerChangeSchema.shape)
|
|
4452
|
+
});
|
|
4453
|
+
const AccountCreated = defineCrmEvent({
|
|
4454
|
+
name: CRM_EVENTS.ACCOUNT_CREATED,
|
|
4455
|
+
resource: "account",
|
|
4456
|
+
description: "createAccount() inserted an account.",
|
|
4457
|
+
zodSchema: z.object({
|
|
4458
|
+
accountId: z.string(),
|
|
4459
|
+
name: z.string()
|
|
4460
|
+
})
|
|
4461
|
+
});
|
|
4462
|
+
const AccountUpdated = defineCrmEvent({
|
|
4463
|
+
name: CRM_EVENTS.ACCOUNT_UPDATED,
|
|
4464
|
+
resource: "account",
|
|
4465
|
+
description: "An account record was updated.",
|
|
4466
|
+
zodSchema: z.object({ accountId: z.string() })
|
|
4467
|
+
});
|
|
4468
|
+
const AccountEnriched = defineCrmEvent({
|
|
4469
|
+
name: CRM_EVENTS.ACCOUNT_ENRICHED,
|
|
4470
|
+
resource: "account",
|
|
4471
|
+
description: "enrich() merged a patch into the account.",
|
|
4472
|
+
zodSchema: z.object({ accountId: z.string() }).extend(enrichSchema.shape)
|
|
4473
|
+
});
|
|
4474
|
+
const AccountAssigned = defineCrmEvent({
|
|
4475
|
+
name: CRM_EVENTS.ACCOUNT_ASSIGNED,
|
|
4476
|
+
resource: "account",
|
|
4477
|
+
description: "assign() (re)assigned the account owner.",
|
|
4478
|
+
zodSchema: z.object({ accountId: z.string() }).extend(ownerChangeSchema.shape)
|
|
4479
|
+
});
|
|
4480
|
+
const AccountMerged = defineCrmEvent({
|
|
4481
|
+
name: CRM_EVENTS.ACCOUNT_MERGED,
|
|
4482
|
+
resource: "account",
|
|
4483
|
+
description: "merge() folded a secondary account into the primary; secondary deleted.",
|
|
4484
|
+
zodSchema: mergeSchema
|
|
4485
|
+
});
|
|
4486
|
+
const ContactCreated = defineCrmEvent({
|
|
4487
|
+
name: CRM_EVENTS.CONTACT_CREATED,
|
|
4488
|
+
resource: "contact",
|
|
4489
|
+
description: "createContact() inserted a contact.",
|
|
4490
|
+
zodSchema: z.object({
|
|
4491
|
+
contactId: z.string(),
|
|
4492
|
+
email: z.string().optional()
|
|
4493
|
+
})
|
|
4494
|
+
});
|
|
4495
|
+
const ContactUpdated = defineCrmEvent({
|
|
4496
|
+
name: CRM_EVENTS.CONTACT_UPDATED,
|
|
4497
|
+
resource: "contact",
|
|
4498
|
+
description: "A contact record was updated.",
|
|
4499
|
+
zodSchema: z.object({ contactId: z.string() })
|
|
4500
|
+
});
|
|
4501
|
+
const ContactEnriched = defineCrmEvent({
|
|
4502
|
+
name: CRM_EVENTS.CONTACT_ENRICHED,
|
|
4503
|
+
resource: "contact",
|
|
4504
|
+
description: "enrich() merged a patch into the contact.",
|
|
4505
|
+
zodSchema: z.object({ contactId: z.string() }).extend(enrichSchema.shape)
|
|
4506
|
+
});
|
|
4507
|
+
const ContactAssigned = defineCrmEvent({
|
|
4508
|
+
name: CRM_EVENTS.CONTACT_ASSIGNED,
|
|
4509
|
+
resource: "contact",
|
|
4510
|
+
description: "assign() (re)assigned the contact owner.",
|
|
4511
|
+
zodSchema: z.object({ contactId: z.string() }).extend(ownerChangeSchema.shape)
|
|
4512
|
+
});
|
|
4513
|
+
const ContactMerged = defineCrmEvent({
|
|
4514
|
+
name: CRM_EVENTS.CONTACT_MERGED,
|
|
4515
|
+
resource: "contact",
|
|
4516
|
+
description: "merge() folded a secondary contact into the primary; secondary deleted.",
|
|
4517
|
+
zodSchema: mergeSchema
|
|
4518
|
+
});
|
|
4519
|
+
const OpportunityCreated = defineCrmEvent({
|
|
4520
|
+
name: CRM_EVENTS.OPPORTUNITY_CREATED,
|
|
4521
|
+
resource: "opportunity",
|
|
4522
|
+
description: "openOpportunity() created a deal in a pipeline stage.",
|
|
4523
|
+
zodSchema: z.object({
|
|
4524
|
+
opportunityId: z.string(),
|
|
4525
|
+
pipelineId: z.string(),
|
|
4526
|
+
stageId: z.string()
|
|
4527
|
+
})
|
|
4528
|
+
});
|
|
4529
|
+
const OpportunityStageAdvanced = defineCrmEvent({
|
|
4530
|
+
name: CRM_EVENTS.OPPORTUNITY_STAGE_ADVANCED,
|
|
4531
|
+
resource: "opportunity",
|
|
4532
|
+
description: "moveToStage() moved the deal forward (higher stage sequence).",
|
|
4533
|
+
zodSchema: z.object({
|
|
4534
|
+
opportunityId: z.string(),
|
|
4535
|
+
from: stageRefSchema,
|
|
4536
|
+
to: stageRefSchema
|
|
4537
|
+
})
|
|
4538
|
+
});
|
|
4539
|
+
const OpportunityStageRegressed = defineCrmEvent({
|
|
4540
|
+
name: CRM_EVENTS.OPPORTUNITY_STAGE_REGRESSED,
|
|
4541
|
+
resource: "opportunity",
|
|
4542
|
+
description: "moveToStage() moved the deal backward (lower stage sequence).",
|
|
4543
|
+
zodSchema: z.object({
|
|
4544
|
+
opportunityId: z.string(),
|
|
4545
|
+
from: stageRefSchema,
|
|
4546
|
+
to: stageRefSchema
|
|
4547
|
+
})
|
|
4548
|
+
});
|
|
4549
|
+
const OpportunityWon = defineCrmEvent({
|
|
4550
|
+
name: CRM_EVENTS.OPPORTUNITY_WON,
|
|
4551
|
+
resource: "opportunity",
|
|
4552
|
+
description: "win() closed the deal as won (terminal).",
|
|
4553
|
+
zodSchema: z.object({
|
|
4554
|
+
opportunityId: z.string(),
|
|
4555
|
+
amount: z.number().optional()
|
|
4556
|
+
})
|
|
4557
|
+
});
|
|
4558
|
+
const OpportunityLost = defineCrmEvent({
|
|
4559
|
+
name: CRM_EVENTS.OPPORTUNITY_LOST,
|
|
4560
|
+
resource: "opportunity",
|
|
4561
|
+
description: "lose() closed the deal as lost (terminal).",
|
|
4562
|
+
zodSchema: z.object({
|
|
4563
|
+
opportunityId: z.string(),
|
|
4564
|
+
lostReasonId: z.string().optional()
|
|
4565
|
+
})
|
|
4566
|
+
});
|
|
4567
|
+
const OpportunityAbandoned = defineCrmEvent({
|
|
4568
|
+
name: CRM_EVENTS.OPPORTUNITY_ABANDONED,
|
|
4569
|
+
resource: "opportunity",
|
|
4570
|
+
description: "abandon() closed the deal as abandoned (terminal).",
|
|
4571
|
+
zodSchema: z.object({ opportunityId: z.string() })
|
|
4572
|
+
});
|
|
4573
|
+
const OpportunityAssigned = defineCrmEvent({
|
|
4574
|
+
name: CRM_EVENTS.OPPORTUNITY_ASSIGNED,
|
|
4575
|
+
resource: "opportunity",
|
|
4576
|
+
description: "assign() (re)assigned the opportunity owner.",
|
|
4577
|
+
zodSchema: z.object({ opportunityId: z.string() }).extend(ownerChangeSchema.shape)
|
|
4578
|
+
});
|
|
4579
|
+
const OpportunityAmountChanged = defineCrmEvent({
|
|
4580
|
+
name: CRM_EVENTS.OPPORTUNITY_AMOUNT_CHANGED,
|
|
4581
|
+
resource: "opportunity",
|
|
4582
|
+
description: "updateAmount() changed an open deal value.",
|
|
4583
|
+
zodSchema: z.object({
|
|
4584
|
+
opportunityId: z.string(),
|
|
4585
|
+
amount: z.number(),
|
|
4586
|
+
previousAmount: z.number().optional(),
|
|
4587
|
+
reason: z.string().optional(),
|
|
4588
|
+
by: z.string().optional()
|
|
4589
|
+
})
|
|
4590
|
+
});
|
|
4591
|
+
const OpportunityExtended = defineCrmEvent({
|
|
4592
|
+
name: CRM_EVENTS.OPPORTUNITY_EXTENDED,
|
|
4593
|
+
resource: "opportunity",
|
|
4594
|
+
description: "extendCloseDate() pushed expectedCloseAt further out.",
|
|
4595
|
+
zodSchema: z.object({
|
|
4596
|
+
opportunityId: z.string(),
|
|
4597
|
+
expectedCloseAt: z.iso.datetime(),
|
|
4598
|
+
previousExpectedCloseAt: z.iso.datetime().optional(),
|
|
4599
|
+
reason: z.string().optional(),
|
|
4600
|
+
by: z.string().optional()
|
|
4601
|
+
})
|
|
4602
|
+
});
|
|
4603
|
+
const PipelineCreated = defineCrmEvent({
|
|
4604
|
+
name: CRM_EVENTS.PIPELINE_CREATED,
|
|
4605
|
+
resource: "pipeline",
|
|
4606
|
+
description: "A pipeline was created.",
|
|
4607
|
+
zodSchema: z.object({ pipelineId: z.string() })
|
|
4608
|
+
});
|
|
4609
|
+
const PipelineArchived = defineCrmEvent({
|
|
4610
|
+
name: CRM_EVENTS.PIPELINE_ARCHIVED,
|
|
4611
|
+
resource: "pipeline",
|
|
4612
|
+
description: "archive() soft-deprecated a pipeline.",
|
|
4613
|
+
zodSchema: z.object({ pipelineId: z.string() })
|
|
4614
|
+
});
|
|
4615
|
+
const ActivityPlanned = defineCrmEvent({
|
|
4616
|
+
name: CRM_EVENTS.ACTIVITY_PLANNED,
|
|
4617
|
+
resource: "activity",
|
|
4618
|
+
description: "plan() created a planned activity against a subject.",
|
|
4619
|
+
zodSchema: z.object({
|
|
4620
|
+
activityId: z.string(),
|
|
4621
|
+
type: z.string(),
|
|
4622
|
+
subjectKind: z.string(),
|
|
4623
|
+
subjectId: z.string()
|
|
4624
|
+
})
|
|
4625
|
+
});
|
|
4626
|
+
const ActivityCompleted = defineCrmEvent({
|
|
4627
|
+
name: CRM_EVENTS.ACTIVITY_COMPLETED,
|
|
4628
|
+
resource: "activity",
|
|
4629
|
+
description: "complete() finished an activity.",
|
|
4630
|
+
zodSchema: z.object({
|
|
4631
|
+
activityId: z.string(),
|
|
4632
|
+
completedAt: z.iso.datetime()
|
|
4633
|
+
})
|
|
4634
|
+
});
|
|
4635
|
+
const ActivityCancelled = defineCrmEvent({
|
|
4636
|
+
name: CRM_EVENTS.ACTIVITY_CANCELLED,
|
|
4637
|
+
resource: "activity",
|
|
4638
|
+
description: "cancel() cancelled an activity.",
|
|
4639
|
+
zodSchema: z.object({
|
|
4640
|
+
activityId: z.string(),
|
|
4641
|
+
reason: z.string().optional()
|
|
4642
|
+
})
|
|
4643
|
+
});
|
|
4644
|
+
const ActivityRescheduled = defineCrmEvent({
|
|
4645
|
+
name: CRM_EVENTS.ACTIVITY_RESCHEDULED,
|
|
4646
|
+
resource: "activity",
|
|
4647
|
+
description: "reschedule() moved an activity due date.",
|
|
4648
|
+
zodSchema: z.object({
|
|
4649
|
+
activityId: z.string(),
|
|
4650
|
+
dueDate: z.iso.datetime(),
|
|
4651
|
+
previousDueDate: z.iso.datetime().optional(),
|
|
4652
|
+
reason: z.string().optional(),
|
|
4653
|
+
by: z.string().optional()
|
|
4654
|
+
})
|
|
4655
|
+
});
|
|
4656
|
+
const ActivityOutcomeRecorded = defineCrmEvent({
|
|
4657
|
+
name: CRM_EVENTS.ACTIVITY_OUTCOME_RECORDED,
|
|
4658
|
+
resource: "activity",
|
|
4659
|
+
description: "recordOutcome() logged an activity outcome (no status change).",
|
|
4660
|
+
zodSchema: z.object({
|
|
4661
|
+
activityId: z.string(),
|
|
4662
|
+
outcome: z.record(z.string(), z.unknown())
|
|
4663
|
+
})
|
|
4664
|
+
});
|
|
4665
|
+
const NoteAdded = defineCrmEvent({
|
|
4666
|
+
name: CRM_EVENTS.NOTE_ADDED,
|
|
4667
|
+
resource: "note",
|
|
4668
|
+
description: "A note was appended to a subject timeline.",
|
|
4669
|
+
zodSchema: z.object({
|
|
4670
|
+
noteId: z.string(),
|
|
4671
|
+
subjectKind: z.string(),
|
|
4672
|
+
subjectId: z.string()
|
|
4673
|
+
})
|
|
4674
|
+
});
|
|
4675
|
+
const CampaignCreated = defineCrmEvent({
|
|
4676
|
+
name: CRM_EVENTS.CAMPAIGN_CREATED,
|
|
4677
|
+
resource: "campaign",
|
|
4678
|
+
description: "createCampaign() inserted a campaign.",
|
|
4679
|
+
zodSchema: z.object({
|
|
4680
|
+
campaignId: z.string(),
|
|
4681
|
+
name: z.string()
|
|
4682
|
+
})
|
|
4683
|
+
});
|
|
4684
|
+
const CampaignActivated = defineCrmEvent({
|
|
4685
|
+
name: CRM_EVENTS.CAMPAIGN_ACTIVATED,
|
|
4686
|
+
resource: "campaign",
|
|
4687
|
+
description: "activate() moved a campaign to active.",
|
|
4688
|
+
zodSchema: z.object({ campaignId: z.string() })
|
|
4689
|
+
});
|
|
4690
|
+
const CampaignPaused = defineCrmEvent({
|
|
4691
|
+
name: CRM_EVENTS.CAMPAIGN_PAUSED,
|
|
4692
|
+
resource: "campaign",
|
|
4693
|
+
description: "pause() suspended an active campaign.",
|
|
4694
|
+
zodSchema: z.object({
|
|
4695
|
+
campaignId: z.string(),
|
|
4696
|
+
reason: z.string().optional()
|
|
4697
|
+
})
|
|
4698
|
+
});
|
|
4699
|
+
const CampaignResumed = defineCrmEvent({
|
|
4700
|
+
name: CRM_EVENTS.CAMPAIGN_RESUMED,
|
|
4701
|
+
resource: "campaign",
|
|
4702
|
+
description: "resume() reactivated a paused campaign.",
|
|
4703
|
+
zodSchema: z.object({ campaignId: z.string() })
|
|
4704
|
+
});
|
|
4705
|
+
const CampaignCompleted = defineCrmEvent({
|
|
4706
|
+
name: CRM_EVENTS.CAMPAIGN_COMPLETED,
|
|
4707
|
+
resource: "campaign",
|
|
4708
|
+
description: "complete() closed a campaign.",
|
|
4709
|
+
zodSchema: z.object({
|
|
4710
|
+
campaignId: z.string(),
|
|
4711
|
+
actualRevenueAmount: z.number().optional()
|
|
4712
|
+
})
|
|
4713
|
+
});
|
|
4714
|
+
const CampaignArchived = defineCrmEvent({
|
|
4715
|
+
name: CRM_EVENTS.CAMPAIGN_ARCHIVED,
|
|
4716
|
+
resource: "campaign",
|
|
4717
|
+
description: "archive() archived a campaign (terminal).",
|
|
4718
|
+
zodSchema: z.object({ campaignId: z.string() })
|
|
4719
|
+
});
|
|
4720
|
+
const SubscriptionCreated = defineCrmEvent({
|
|
4721
|
+
name: CRM_EVENTS.SUBSCRIPTION_CREATED,
|
|
4722
|
+
resource: "subscription",
|
|
4723
|
+
description: "subscribe() created an opt-in (pending or confirmed).",
|
|
4724
|
+
zodSchema: z.object({
|
|
4725
|
+
subscriptionId: z.string(),
|
|
4726
|
+
contactId: z.string(),
|
|
4727
|
+
campaignId: z.string(),
|
|
4728
|
+
requiresConfirmation: z.boolean()
|
|
4729
|
+
})
|
|
4730
|
+
});
|
|
4731
|
+
const SubscriptionConfirmed = defineCrmEvent({
|
|
4732
|
+
name: CRM_EVENTS.SUBSCRIPTION_CONFIRMED,
|
|
4733
|
+
resource: "subscription",
|
|
4734
|
+
description: "confirm() validated a double-opt-in token.",
|
|
4735
|
+
zodSchema: z.object({
|
|
4736
|
+
subscriptionId: z.string(),
|
|
4737
|
+
contactId: z.string(),
|
|
4738
|
+
campaignId: z.string(),
|
|
4739
|
+
confirmedAt: z.iso.datetime()
|
|
4740
|
+
})
|
|
4741
|
+
});
|
|
4742
|
+
const SubscriptionUnsubscribed = defineCrmEvent({
|
|
4743
|
+
name: CRM_EVENTS.SUBSCRIPTION_UNSUBSCRIBED,
|
|
4744
|
+
resource: "subscription",
|
|
4745
|
+
description: "unsubscribe() opted a contact out (terminal).",
|
|
4746
|
+
zodSchema: z.object({
|
|
4747
|
+
subscriptionId: z.string(),
|
|
4748
|
+
contactId: z.string(),
|
|
4749
|
+
campaignId: z.string(),
|
|
4750
|
+
reason: z.string().optional()
|
|
4751
|
+
})
|
|
4752
|
+
});
|
|
4753
|
+
const SubscriptionBounced = defineCrmEvent({
|
|
4754
|
+
name: CRM_EVENTS.SUBSCRIPTION_BOUNCED,
|
|
4755
|
+
resource: "subscription",
|
|
4756
|
+
description: "markBounced() recorded a hard bounce (terminal).",
|
|
4757
|
+
zodSchema: z.object({
|
|
4758
|
+
subscriptionId: z.string(),
|
|
4759
|
+
contactId: z.string(),
|
|
4760
|
+
campaignId: z.string(),
|
|
4761
|
+
reason: z.string().optional()
|
|
4762
|
+
})
|
|
4763
|
+
});
|
|
4764
|
+
const SubscriptionComplained = defineCrmEvent({
|
|
4765
|
+
name: CRM_EVENTS.SUBSCRIPTION_COMPLAINED,
|
|
4766
|
+
resource: "subscription",
|
|
4767
|
+
description: "markComplained() recorded a spam complaint (terminal).",
|
|
4768
|
+
zodSchema: z.object({
|
|
4769
|
+
subscriptionId: z.string(),
|
|
4770
|
+
contactId: z.string(),
|
|
4771
|
+
campaignId: z.string()
|
|
4772
|
+
})
|
|
4773
|
+
});
|
|
4774
|
+
const SuppressionAdded = defineCrmEvent({
|
|
4775
|
+
name: CRM_EVENTS.SUPPRESSION_ADDED,
|
|
4776
|
+
resource: "suppression",
|
|
4777
|
+
description: "suppress() inserted a NEW row on the org-wide suppression list (idempotent re-suppress of an existing email emits nothing). organizationId rides event meta.",
|
|
4778
|
+
zodSchema: z.object({
|
|
4779
|
+
email: z.string(),
|
|
4780
|
+
reason: z.enum(SUPPRESSION_REASONS),
|
|
4781
|
+
source: z.string().optional()
|
|
4782
|
+
})
|
|
4783
|
+
});
|
|
4784
|
+
const SuppressionRemoved = defineCrmEvent({
|
|
4785
|
+
name: CRM_EVENTS.SUPPRESSION_REMOVED,
|
|
4786
|
+
resource: "suppression",
|
|
4787
|
+
description: "unsuppress() removed an email from the org-wide suppression list.",
|
|
4788
|
+
zodSchema: z.object({ email: z.string() })
|
|
4789
|
+
});
|
|
4790
|
+
const emailEventSchema = z.object({
|
|
4791
|
+
eventId: z.string(),
|
|
4792
|
+
kind: z.string(),
|
|
4793
|
+
subjectKind: z.string(),
|
|
4794
|
+
subjectId: z.string(),
|
|
4795
|
+
campaignId: z.string().optional()
|
|
4796
|
+
});
|
|
4797
|
+
const EmailEventOpened = defineCrmEvent({
|
|
4798
|
+
name: CRM_EVENTS.EMAIL_EVENT_OPENED,
|
|
4799
|
+
resource: "email_event",
|
|
4800
|
+
description: "recordOpen() logged an email open.",
|
|
4801
|
+
zodSchema: emailEventSchema
|
|
4802
|
+
});
|
|
4803
|
+
const EmailEventClicked = defineCrmEvent({
|
|
4804
|
+
name: CRM_EVENTS.EMAIL_EVENT_CLICKED,
|
|
4805
|
+
resource: "email_event",
|
|
4806
|
+
description: "recordClick() logged a link click.",
|
|
4807
|
+
zodSchema: emailEventSchema
|
|
4808
|
+
});
|
|
4809
|
+
const EmailEventBounced = defineCrmEvent({
|
|
4810
|
+
name: CRM_EVENTS.EMAIL_EVENT_BOUNCED,
|
|
4811
|
+
resource: "email_event",
|
|
4812
|
+
description: "recordBounce() logged a bounce.",
|
|
4813
|
+
zodSchema: emailEventSchema
|
|
4814
|
+
});
|
|
4815
|
+
const EmailEventComplained = defineCrmEvent({
|
|
4816
|
+
name: CRM_EVENTS.EMAIL_EVENT_COMPLAINED,
|
|
4817
|
+
resource: "email_event",
|
|
4818
|
+
description: "recordComplaint() logged a spam complaint.",
|
|
4819
|
+
zodSchema: emailEventSchema
|
|
4820
|
+
});
|
|
4821
|
+
const EmailEventUnsubscribed = defineCrmEvent({
|
|
4822
|
+
name: CRM_EVENTS.EMAIL_EVENT_UNSUBSCRIBED,
|
|
4823
|
+
resource: "email_event",
|
|
4824
|
+
description: "recordUnsubscribe() logged a one-click unsubscribe.",
|
|
4825
|
+
zodSchema: emailEventSchema
|
|
4826
|
+
});
|
|
4827
|
+
const ConversationCreated = defineCrmEvent({
|
|
4828
|
+
name: CRM_EVENTS.CONVERSATION_CREATED,
|
|
4829
|
+
resource: "conversation",
|
|
4830
|
+
description: "openConversation() started a conversation thread.",
|
|
4831
|
+
zodSchema: z.object({
|
|
4832
|
+
conversationId: z.string(),
|
|
4833
|
+
channel: z.string(),
|
|
4834
|
+
participantKind: z.string(),
|
|
4835
|
+
participantId: z.string()
|
|
4836
|
+
})
|
|
4837
|
+
});
|
|
4838
|
+
const ConversationAssigned = defineCrmEvent({
|
|
4839
|
+
name: CRM_EVENTS.CONVERSATION_ASSIGNED,
|
|
4840
|
+
resource: "conversation",
|
|
4841
|
+
description: "assign() (re)assigned a conversation owner.",
|
|
4842
|
+
zodSchema: z.object({
|
|
4843
|
+
conversationId: z.string(),
|
|
4844
|
+
assigneeId: z.string(),
|
|
4845
|
+
previousAssigneeId: z.string().optional()
|
|
4846
|
+
})
|
|
4847
|
+
});
|
|
4848
|
+
const ConversationResolved = defineCrmEvent({
|
|
4849
|
+
name: CRM_EVENTS.CONVERSATION_RESOLVED,
|
|
4850
|
+
resource: "conversation",
|
|
4851
|
+
description: "resolve() closed a conversation.",
|
|
4852
|
+
zodSchema: z.object({
|
|
4853
|
+
conversationId: z.string(),
|
|
4854
|
+
status: z.string(),
|
|
4855
|
+
previousStatus: z.string()
|
|
4856
|
+
})
|
|
4857
|
+
});
|
|
4858
|
+
const ConversationReopened = defineCrmEvent({
|
|
4859
|
+
name: CRM_EVENTS.CONVERSATION_REOPENED,
|
|
4860
|
+
resource: "conversation",
|
|
4861
|
+
description: "reopen() reopened a resolved conversation.",
|
|
4862
|
+
zodSchema: z.object({
|
|
4863
|
+
conversationId: z.string(),
|
|
4864
|
+
status: z.string(),
|
|
4865
|
+
previousStatus: z.string()
|
|
4866
|
+
})
|
|
4867
|
+
});
|
|
4868
|
+
const MessageReceived = defineCrmEvent({
|
|
4869
|
+
name: CRM_EVENTS.MESSAGE_RECEIVED,
|
|
4870
|
+
resource: "message",
|
|
4871
|
+
description: "recordInbound() logged an inbound message.",
|
|
4872
|
+
zodSchema: z.object({
|
|
4873
|
+
messageId: z.string(),
|
|
4874
|
+
conversationId: z.string(),
|
|
4875
|
+
direction: z.string(),
|
|
4876
|
+
externalMessageId: z.string().optional()
|
|
4877
|
+
})
|
|
4878
|
+
});
|
|
4879
|
+
const MessageSent = defineCrmEvent({
|
|
4880
|
+
name: CRM_EVENTS.MESSAGE_SENT,
|
|
4881
|
+
resource: "message",
|
|
4882
|
+
description: "recordOutbound() logged an outbound message.",
|
|
4883
|
+
zodSchema: z.object({
|
|
4884
|
+
messageId: z.string(),
|
|
4885
|
+
conversationId: z.string(),
|
|
4886
|
+
direction: z.string(),
|
|
4887
|
+
externalMessageId: z.string().optional()
|
|
4888
|
+
})
|
|
4889
|
+
});
|
|
4890
|
+
const SequenceCreated = defineCrmEvent({
|
|
4891
|
+
name: CRM_EVENTS.SEQUENCE_CREATED,
|
|
4892
|
+
resource: "sequence",
|
|
4893
|
+
description: "A sequence draft was created. Emitted by the HOST after repo.create() (the package ships no createSequence verb) — defined here so registries validate the payload.",
|
|
4894
|
+
zodSchema: z.object({
|
|
4895
|
+
sequenceId: z.string(),
|
|
4896
|
+
name: z.string().optional()
|
|
4897
|
+
})
|
|
4898
|
+
});
|
|
4899
|
+
const SequenceActivated = defineCrmEvent({
|
|
4900
|
+
name: CRM_EVENTS.SEQUENCE_ACTIVATED,
|
|
4901
|
+
resource: "sequence",
|
|
4902
|
+
description: "activate() moved a sequence draft/paused → active.",
|
|
4903
|
+
zodSchema: z.object({ sequenceId: z.string() })
|
|
4904
|
+
});
|
|
4905
|
+
const SequencePaused = defineCrmEvent({
|
|
4906
|
+
name: CRM_EVENTS.SEQUENCE_PAUSED,
|
|
4907
|
+
resource: "sequence",
|
|
4908
|
+
description: "pause() suspended an active sequence (existing enrollments hold).",
|
|
4909
|
+
zodSchema: z.object({ sequenceId: z.string() })
|
|
4910
|
+
});
|
|
4911
|
+
const SequenceArchived = defineCrmEvent({
|
|
4912
|
+
name: CRM_EVENTS.SEQUENCE_ARCHIVED,
|
|
4913
|
+
resource: "sequence",
|
|
4914
|
+
description: "archive() retired a sequence — no new enrollments.",
|
|
4915
|
+
zodSchema: z.object({ sequenceId: z.string() })
|
|
4916
|
+
});
|
|
4917
|
+
const EnrollmentCreated = defineCrmEvent({
|
|
4918
|
+
name: CRM_EVENTS.ENROLLMENT_CREATED,
|
|
4919
|
+
resource: "enrollment",
|
|
4920
|
+
description: "enroll() attached a subject to a sequence.",
|
|
4921
|
+
zodSchema: z.object({
|
|
4922
|
+
enrollmentId: z.string(),
|
|
4923
|
+
sequenceId: z.string(),
|
|
4924
|
+
subjectId: z.string(),
|
|
4925
|
+
subjectKind: z.string()
|
|
4926
|
+
})
|
|
4927
|
+
});
|
|
4928
|
+
const EnrollmentStepSent = defineCrmEvent({
|
|
4929
|
+
name: CRM_EVENTS.ENROLLMENT_STEP_SENT,
|
|
4930
|
+
resource: "enrollment",
|
|
4931
|
+
description: "A sequence step was delivered. Emitted by the HOST outreach layer after its send adapter succeeds + advanceStep() commits (Buffer-style pattern in CLAUDE.md) — defined here so registries validate the payload.",
|
|
4932
|
+
zodSchema: z.object({
|
|
4933
|
+
enrollmentId: z.string(),
|
|
4934
|
+
sequenceId: z.string().optional(),
|
|
4935
|
+
stepIndex: z.number(),
|
|
4936
|
+
channel: z.string().optional()
|
|
4937
|
+
})
|
|
4938
|
+
});
|
|
4939
|
+
const EnrollmentPaused = defineCrmEvent({
|
|
4940
|
+
name: CRM_EVENTS.ENROLLMENT_PAUSED,
|
|
4941
|
+
resource: "enrollment",
|
|
4942
|
+
description: "pause() held an active enrollment.",
|
|
4943
|
+
zodSchema: z.object({ enrollmentId: z.string() })
|
|
4944
|
+
});
|
|
4945
|
+
const EnrollmentResumed = defineCrmEvent({
|
|
4946
|
+
name: CRM_EVENTS.ENROLLMENT_RESUMED,
|
|
4947
|
+
resource: "enrollment",
|
|
4948
|
+
description: "resume() reactivated a paused enrollment.",
|
|
4949
|
+
zodSchema: z.object({ enrollmentId: z.string() })
|
|
4950
|
+
});
|
|
4951
|
+
const EnrollmentCompleted = defineCrmEvent({
|
|
4952
|
+
name: CRM_EVENTS.ENROLLMENT_COMPLETED,
|
|
4953
|
+
resource: "enrollment",
|
|
4954
|
+
description: "complete() finished the final step of an enrollment.",
|
|
4955
|
+
zodSchema: z.object({ enrollmentId: z.string() })
|
|
4956
|
+
});
|
|
4957
|
+
const EnrollmentExited = defineCrmEvent({
|
|
4958
|
+
name: CRM_EVENTS.ENROLLMENT_EXITED,
|
|
4959
|
+
resource: "enrollment",
|
|
4960
|
+
description: "exit() removed a subject before completion (reply/unsubscribe/manual/goal).",
|
|
4961
|
+
zodSchema: z.object({
|
|
4962
|
+
enrollmentId: z.string(),
|
|
4963
|
+
exitReason: z.string().optional()
|
|
4964
|
+
})
|
|
4965
|
+
});
|
|
4966
|
+
const crmEventDefinitions = [
|
|
4967
|
+
LeadCreated,
|
|
4968
|
+
LeadStatusChanged,
|
|
4969
|
+
LeadScored,
|
|
4970
|
+
LeadConverted,
|
|
4971
|
+
LeadDisqualified,
|
|
4972
|
+
LeadEnriched,
|
|
4973
|
+
LeadAssigned,
|
|
4974
|
+
AccountCreated,
|
|
4975
|
+
AccountUpdated,
|
|
4976
|
+
AccountEnriched,
|
|
4977
|
+
AccountAssigned,
|
|
4978
|
+
AccountMerged,
|
|
4979
|
+
ContactCreated,
|
|
4980
|
+
ContactUpdated,
|
|
4981
|
+
ContactEnriched,
|
|
4982
|
+
ContactAssigned,
|
|
4983
|
+
ContactMerged,
|
|
4984
|
+
OpportunityCreated,
|
|
4985
|
+
OpportunityStageAdvanced,
|
|
4986
|
+
OpportunityStageRegressed,
|
|
4987
|
+
OpportunityWon,
|
|
4988
|
+
OpportunityLost,
|
|
4989
|
+
OpportunityAbandoned,
|
|
4990
|
+
OpportunityAssigned,
|
|
4991
|
+
OpportunityAmountChanged,
|
|
4992
|
+
OpportunityExtended,
|
|
4993
|
+
PipelineCreated,
|
|
4994
|
+
PipelineArchived,
|
|
4995
|
+
ActivityPlanned,
|
|
4996
|
+
ActivityCompleted,
|
|
4997
|
+
ActivityCancelled,
|
|
4998
|
+
ActivityRescheduled,
|
|
4999
|
+
ActivityOutcomeRecorded,
|
|
5000
|
+
NoteAdded,
|
|
5001
|
+
CampaignCreated,
|
|
5002
|
+
CampaignActivated,
|
|
5003
|
+
CampaignPaused,
|
|
5004
|
+
CampaignResumed,
|
|
5005
|
+
CampaignCompleted,
|
|
5006
|
+
CampaignArchived,
|
|
5007
|
+
SubscriptionCreated,
|
|
5008
|
+
SubscriptionConfirmed,
|
|
5009
|
+
SubscriptionUnsubscribed,
|
|
5010
|
+
SubscriptionBounced,
|
|
5011
|
+
SubscriptionComplained,
|
|
5012
|
+
SuppressionAdded,
|
|
5013
|
+
SuppressionRemoved,
|
|
5014
|
+
EmailEventOpened,
|
|
5015
|
+
EmailEventClicked,
|
|
5016
|
+
EmailEventBounced,
|
|
5017
|
+
EmailEventComplained,
|
|
5018
|
+
EmailEventUnsubscribed,
|
|
5019
|
+
ConversationCreated,
|
|
5020
|
+
ConversationAssigned,
|
|
5021
|
+
ConversationResolved,
|
|
5022
|
+
ConversationReopened,
|
|
5023
|
+
MessageReceived,
|
|
5024
|
+
MessageSent,
|
|
5025
|
+
SequenceCreated,
|
|
5026
|
+
SequenceActivated,
|
|
5027
|
+
SequencePaused,
|
|
5028
|
+
SequenceArchived,
|
|
5029
|
+
EnrollmentCreated,
|
|
5030
|
+
EnrollmentStepSent,
|
|
5031
|
+
EnrollmentPaused,
|
|
5032
|
+
EnrollmentResumed,
|
|
5033
|
+
EnrollmentCompleted,
|
|
5034
|
+
EnrollmentExited
|
|
5035
|
+
];
|
|
5036
|
+
//#endregion
|
|
3403
5037
|
//#region src/shared/calculators/forecast.ts
|
|
3404
5038
|
/**
|
|
5039
|
+
* Pure forecast calculators — no I/O, no Mongo. Hosts call these after
|
|
5040
|
+
* loading opportunities via `repositories.opportunity.list(...)`.
|
|
5041
|
+
*
|
|
5042
|
+
* Amount is flat (`amount` Number + `currency` String per PACKAGE_RULES
|
|
5043
|
+
* §4) — same shape the opportunity schema stores.
|
|
5044
|
+
*/
|
|
5045
|
+
/**
|
|
3405
5046
|
* Aggregate opportunities into a forecast table — one row per bucket.
|
|
3406
5047
|
* Currency mismatch within a bucket throws — partition by currency
|
|
3407
5048
|
* upstream when multi-currency.
|
|
3408
5049
|
*/
|
|
3409
5050
|
function forecast(opportunities, options = {}) {
|
|
3410
|
-
const bucketFn = options.bucket ?? defaultBucket;
|
|
5051
|
+
const bucketFn = options.bucket ?? (options.timeZone !== void 0 ? zoneBucket(options.timeZone) : defaultBucket);
|
|
3411
5052
|
const include = options.include ?? defaultInclude;
|
|
3412
5053
|
const rows = /* @__PURE__ */ new Map();
|
|
3413
5054
|
for (const o of opportunities) {
|
|
@@ -3440,6 +5081,10 @@ function defaultBucket(o) {
|
|
|
3440
5081
|
const d = o.expectedCloseAt;
|
|
3441
5082
|
return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}`;
|
|
3442
5083
|
}
|
|
5084
|
+
/** Month bucket cut at the business's local midnight, not UTC. */
|
|
5085
|
+
function zoneBucket(timeZone) {
|
|
5086
|
+
return (o) => civilDateOf(o.expectedCloseAt, timeZone).slice(0, 7);
|
|
5087
|
+
}
|
|
3443
5088
|
/**
|
|
3444
5089
|
* "Where do deals die?" summary — opportunities partitioned across
|
|
3445
5090
|
* stages with current count + won/lost-from-here.
|
|
@@ -3476,7 +5121,7 @@ const objectIdLike = z.union([z.string().min(1), z.string()]);
|
|
|
3476
5121
|
const metadataSchema = z.record(z.string(), z.unknown()).optional();
|
|
3477
5122
|
const tagsSchema = z.array(z.string().min(1).max(64)).max(50).optional();
|
|
3478
5123
|
const moneyAmountSchema = z.number().int().min(0).max(Number.MAX_SAFE_INTEGER);
|
|
3479
|
-
const currencyCodeSchema = z.string().regex(
|
|
5124
|
+
const currencyCodeSchema = z.string().regex(CURRENCY_PATTERN, "currency must be an ISO 4217 code (3 uppercase letters)");
|
|
3480
5125
|
//#endregion
|
|
3481
5126
|
//#region src/validators/pipeline.schema.ts
|
|
3482
5127
|
const stageInputSchema = z.object({
|
|
@@ -3530,7 +5175,7 @@ const convertLeadSchema = z.object({
|
|
|
3530
5175
|
pipelineId: objectIdLike,
|
|
3531
5176
|
opportunityName: z.string().min(1).max(200).optional(),
|
|
3532
5177
|
amount: z.number().nonnegative().optional(),
|
|
3533
|
-
currency:
|
|
5178
|
+
currency: currencyCodeSchema.optional(),
|
|
3534
5179
|
expectedCloseAt: z.coerce.date().optional(),
|
|
3535
5180
|
by: z.string().optional()
|
|
3536
5181
|
});
|
|
@@ -3686,6 +5331,21 @@ const subscriptionFilterSchema = z.object({
|
|
|
3686
5331
|
campaignId: objectIdLike.optional()
|
|
3687
5332
|
});
|
|
3688
5333
|
//#endregion
|
|
5334
|
+
//#region src/validators/suppression.schema.ts
|
|
5335
|
+
const suppressSchema = z.object({
|
|
5336
|
+
email: z.string().email().max(320),
|
|
5337
|
+
reason: z.enum(SUPPRESSION_REASONS),
|
|
5338
|
+
source: z.string().max(200).optional(),
|
|
5339
|
+
subjectKind: z.string().max(50).optional(),
|
|
5340
|
+
subjectId: z.string().max(100).optional(),
|
|
5341
|
+
metadata: metadataSchema
|
|
5342
|
+
});
|
|
5343
|
+
const unsuppressSchema = z.object({ email: z.string().email().max(320) });
|
|
5344
|
+
const suppressionFilterSchema = z.object({
|
|
5345
|
+
email: z.string().max(320).optional(),
|
|
5346
|
+
reason: z.enum(SUPPRESSION_REASONS).optional()
|
|
5347
|
+
});
|
|
5348
|
+
//#endregion
|
|
3689
5349
|
//#region src/validators/activity.schema.ts
|
|
3690
5350
|
const planActivitySchema = z.object({
|
|
3691
5351
|
type: z.enum(ACTIVITY_TYPES),
|
|
@@ -3762,6 +5422,54 @@ const emailEventFilterSchema = z.object({
|
|
|
3762
5422
|
messageId: z.string().optional()
|
|
3763
5423
|
});
|
|
3764
5424
|
//#endregion
|
|
5425
|
+
//#region src/validators/sequence.schema.ts
|
|
5426
|
+
const sendEmailStepSchema = z.object({
|
|
5427
|
+
id: z.string().min(1),
|
|
5428
|
+
type: z.literal("SEND_EMAIL"),
|
|
5429
|
+
name: z.string().max(100).optional(),
|
|
5430
|
+
subject: z.string().min(1).max(300),
|
|
5431
|
+
htmlBody: z.string().min(1),
|
|
5432
|
+
fromName: z.string().max(100).optional(),
|
|
5433
|
+
fromEmail: z.string().email().optional()
|
|
5434
|
+
});
|
|
5435
|
+
const delayStepSchema = z.object({
|
|
5436
|
+
id: z.string().min(1),
|
|
5437
|
+
type: z.literal("DELAY"),
|
|
5438
|
+
name: z.string().max(100).optional(),
|
|
5439
|
+
settings: z.object({
|
|
5440
|
+
days: z.number().int().min(0).optional(),
|
|
5441
|
+
hours: z.number().int().min(0).optional(),
|
|
5442
|
+
minutes: z.number().int().min(0).optional()
|
|
5443
|
+
})
|
|
5444
|
+
});
|
|
5445
|
+
const sequenceStepSchema = z.discriminatedUnion("type", [sendEmailStepSchema, delayStepSchema]);
|
|
5446
|
+
const createSequenceSchema = z.object({
|
|
5447
|
+
name: z.string().min(1).max(200),
|
|
5448
|
+
description: z.string().max(1e3).optional(),
|
|
5449
|
+
fromName: z.string().max(100).optional(),
|
|
5450
|
+
fromEmail: z.string().email().optional(),
|
|
5451
|
+
steps: z.array(sequenceStepSchema).default([]),
|
|
5452
|
+
replyDetectionEnabled: z.boolean().default(true),
|
|
5453
|
+
tags: tagsSchema,
|
|
5454
|
+
ownerId: z.string().optional(),
|
|
5455
|
+
metadata: metadataSchema
|
|
5456
|
+
});
|
|
5457
|
+
const updateSequenceSchema = createSequenceSchema.partial();
|
|
5458
|
+
const sequenceFilterSchema = z.object({
|
|
5459
|
+
status: z.enum([
|
|
5460
|
+
"draft",
|
|
5461
|
+
"active",
|
|
5462
|
+
"paused",
|
|
5463
|
+
"archived"
|
|
5464
|
+
]).optional(),
|
|
5465
|
+
ownerId: z.string().optional(),
|
|
5466
|
+
tags: z.string().optional(),
|
|
5467
|
+
search: z.string().optional(),
|
|
5468
|
+
page: z.coerce.number().int().min(1).optional(),
|
|
5469
|
+
limit: z.coerce.number().int().min(1).max(200).optional(),
|
|
5470
|
+
sort: z.string().optional()
|
|
5471
|
+
});
|
|
5472
|
+
//#endregion
|
|
3765
5473
|
//#region src/validators/index.ts
|
|
3766
5474
|
var validators_exports = /* @__PURE__ */ __exportAll({
|
|
3767
5475
|
accountFilterSchema: () => accountFilterSchema,
|
|
@@ -3781,6 +5489,7 @@ var validators_exports = /* @__PURE__ */ __exportAll({
|
|
|
3781
5489
|
createLeadSchema: () => createLeadSchema,
|
|
3782
5490
|
createLossReasonSchema: () => createLossReasonSchema,
|
|
3783
5491
|
createPipelineSchema: () => createPipelineSchema,
|
|
5492
|
+
createSequenceSchema: () => createSequenceSchema,
|
|
3784
5493
|
currencyCodeSchema: () => currencyCodeSchema,
|
|
3785
5494
|
emailEventFilterSchema: () => emailEventFilterSchema,
|
|
3786
5495
|
leadFilterSchema: () => leadFilterSchema,
|
|
@@ -3795,11 +5504,16 @@ var validators_exports = /* @__PURE__ */ __exportAll({
|
|
|
3795
5504
|
pipelineFilterSchema: () => pipelineFilterSchema,
|
|
3796
5505
|
planActivitySchema: () => planActivitySchema,
|
|
3797
5506
|
recordEmailEventSchema: () => recordEmailEventSchema,
|
|
5507
|
+
sequenceFilterSchema: () => sequenceFilterSchema,
|
|
5508
|
+
sequenceStepSchema: () => sequenceStepSchema,
|
|
3798
5509
|
stageInputSchemaExport: () => stageInputSchemaExport,
|
|
3799
5510
|
subscribeSchema: () => subscribeSchema,
|
|
3800
5511
|
subscriptionFilterSchema: () => subscriptionFilterSchema,
|
|
5512
|
+
suppressSchema: () => suppressSchema,
|
|
5513
|
+
suppressionFilterSchema: () => suppressionFilterSchema,
|
|
3801
5514
|
tagsSchema: () => tagsSchema,
|
|
3802
5515
|
unsubscribeSchema: () => unsubscribeSchema,
|
|
5516
|
+
unsuppressSchema: () => unsuppressSchema,
|
|
3803
5517
|
updateAccountSchema: () => updateAccountSchema,
|
|
3804
5518
|
updateActivitySchema: () => updateActivitySchema,
|
|
3805
5519
|
updateCampaignSchema: () => updateCampaignSchema,
|
|
@@ -3807,7 +5521,8 @@ var validators_exports = /* @__PURE__ */ __exportAll({
|
|
|
3807
5521
|
updateLeadSchema: () => updateLeadSchema,
|
|
3808
5522
|
updateLossReasonSchema: () => updateLossReasonSchema,
|
|
3809
5523
|
updateNoteSchema: () => updateNoteSchema,
|
|
3810
|
-
updatePipelineSchema: () => updatePipelineSchema
|
|
5524
|
+
updatePipelineSchema: () => updatePipelineSchema,
|
|
5525
|
+
updateSequenceSchema: () => updateSequenceSchema
|
|
3811
5526
|
});
|
|
3812
5527
|
//#endregion
|
|
3813
|
-
export { ACTIVITY_MACHINE, ACTIVITY_STATUSES, ACTIVITY_TRANSITIONS, ACTIVITY_TYPES, AccountAssigned, AccountCreated, AccountEnriched, AccountMerged, AccountRepository, AccountUpdated, ActivityCancelled, ActivityCompleted, ActivityOutcomeRecorded, ActivityPlanned, ActivityRepository, ActivityRescheduled, CAMPAIGN_MACHINE, CAMPAIGN_STATUSES, CAMPAIGN_TRANSITIONS, CONVERSATION_CHANNELS, CONVERSATION_PARTICIPANT_KINDS, CONVERSATION_STATUSES, CRM_EVENTS, CRM_MODEL_NAMES, CampaignActivated, CampaignArchived, CampaignCompleted, CampaignCreated, CampaignPaused, CampaignRepository, CampaignResumed, ContactAssigned, ContactCreated, ContactEnriched, ContactMerged, ContactRepository, ContactUpdated, ConversationAssigned, ConversationCreated, ConversationReopened, ConversationRepository, ConversationResolved, CrmError, DuplicateCampaignError, DuplicateLeadError, DuplicateSubscriptionError, EMAIL_EVENT_KINDS, EMAIL_EVENT_SUBJECT_KINDS, EmailEventBounced, EmailEventClicked, EmailEventComplained, EmailEventOpened, EmailEventRepository, EmailEventUnsubscribed, EmptyPipelineError, InProcessCrmBus, InvalidActivityTransitionError, InvalidCampaignTransitionError, InvalidConfirmationTokenError, InvalidLeadTransitionError, InvalidOpportunityTransitionError, InvalidSubscriptionTransitionError, InvalidUnsubscribeTokenError, LEAD_MACHINE, LEAD_STATUSES, LEAD_TRANSITIONS, LeadAlreadyConvertedError, LeadAssigned, LeadConverted, LeadCreated, LeadDisqualified, LeadEnriched, LeadRepository, LeadScored, LeadService, LeadStatusChanged, LossReasonRepository, MESSAGE_DELIVERY_STATUSES, MESSAGE_DIRECTIONS, MessageReceived, MessageRepository, MessageSent, NotFoundError, NoteAdded, NoteRepository, OPPORTUNITY_MACHINE, OPPORTUNITY_STATUSES, OPPORTUNITY_TRANSITIONS, OpportunityAbandoned, OpportunityAmountChanged, OpportunityAssigned, OpportunityCreated, OpportunityExtended, OpportunityLost, OpportunityRepository, OpportunityStageAdvanced, OpportunityStageRegressed, OpportunityWon, PipelineArchived, PipelineCreated, PipelineRepository, SUBJECT_KINDS, SUBSCRIPTION_MACHINE, SUBSCRIPTION_STATUSES, SUBSCRIPTION_TRANSITIONS, StageNotInPipelineError, SubscriptionBounced, SubscriptionComplained, SubscriptionConfirmed, SubscriptionCreated, SubscriptionRepository, SubscriptionUnsubscribed, appendStatus, buildAccountSchema, buildActivitySchema, buildCampaignSchema, buildContactSchema, buildConversationSchema, buildEmailEventSchema, buildLeadSchema, buildLossReasonSchema, buildMessageSchema, buildNoteSchema, buildOpportunitySchema, buildPipelineSchema, buildSubscriptionSchema, canTransitionActivity, canTransitionCampaign, canTransitionLead, canTransitionOpportunity, canTransitionSubscription, composeScorers, createAccountModel, createActivityModel, createCampaignModel, createContactModel, createConversationModel, createCrm, createCrmModels, createCrmRepositories, createEmailEventModel, createEvent, createLeadModel, createLossReasonModel, createMessageModel, createNoteModel, createOpportunityModel, createPipelineModel, createSubscriptionModel, crmEventDefinitions, defaultLeadScorer, forecast, lastTransitionTo, latestEntry, pipelineConversion, tenantFieldDef, timeInStatus, validators_exports as validators };
|
|
5528
|
+
export { ACTIVITY_MACHINE, ACTIVITY_STATUSES, ACTIVITY_TRANSITIONS, ACTIVITY_TYPES, AccountAssigned, AccountCreated, AccountEnriched, AccountMerged, AccountRepository, AccountUpdated, ActivityCancelled, ActivityCompleted, ActivityOutcomeRecorded, ActivityPlanned, ActivityRepository, ActivityRescheduled, AlreadyEnrolledError, CAMPAIGN_MACHINE, CAMPAIGN_STATUSES, CAMPAIGN_TRANSITIONS, CONVERSATION_CHANNELS, CONVERSATION_PARTICIPANT_KINDS, CONVERSATION_STATUSES, CRM_EVENTS, CRM_MODEL_NAMES, CampaignActivated, CampaignArchived, CampaignCompleted, CampaignCreated, CampaignPaused, CampaignRepository, CampaignResumed, ContactAssigned, ContactCreated, ContactEnriched, ContactMerged, ContactRepository, ContactUpdated, ConversationAssigned, ConversationCreated, ConversationReopened, ConversationRepository, ConversationResolved, CrmError, CrmEventDispatcher, DuplicateCampaignError, DuplicateLeadError, DuplicateSubscriptionError, EMAIL_EVENT_KINDS, EMAIL_EVENT_SUBJECT_KINDS, ENROLLMENT_MACHINE, ENROLLMENT_STATUSES, ENROLLMENT_SUBJECT_KINDS, ENROLLMENT_TRANSITIONS, EVENT_DERIVED_SUPPRESSION_REASONS, EmailEventBounced, EmailEventClicked, EmailEventComplained, EmailEventOpened, EmailEventRepository, EmailEventUnsubscribed, EmptyPipelineError, EnrollmentCompleted, EnrollmentCreated, EnrollmentExited, EnrollmentPaused, EnrollmentRepository, EnrollmentResumed, EnrollmentStepSent, InProcessCrmBus, InvalidActivityTransitionError, InvalidCampaignTransitionError, InvalidConfirmationTokenError, InvalidEnrollmentTransitionError, InvalidLeadTransitionError, InvalidOpportunityTransitionError, InvalidSequenceTransitionError, InvalidSubscriptionTransitionError, InvalidUnsubscribeTokenError, LEAD_MACHINE, LEAD_STATUSES, LEAD_TRANSITIONS, LeadAlreadyConvertedError, LeadAssigned, LeadConverted, LeadCreated, LeadDisqualified, LeadEnriched, LeadRepository, LeadScored, LeadService, LeadStatusChanged, LossReasonRepository, MESSAGE_DELIVERY_STATUSES, MESSAGE_DIRECTIONS, MessageReceived, MessageRepository, MessageSent, NotFoundError, NoteAdded, NoteRepository, OPPORTUNITY_MACHINE, OPPORTUNITY_STATUSES, OPPORTUNITY_TRANSITIONS, OpportunityAbandoned, OpportunityAmountChanged, OpportunityAssigned, OpportunityCreated, OpportunityExtended, OpportunityLost, OpportunityRepository, OpportunityStageAdvanced, OpportunityStageRegressed, OpportunityWon, PENDING_EVENTS, PipelineArchived, PipelineCreated, PipelineRepository, SEQUENCE_MACHINE, SEQUENCE_STATUSES, SEQUENCE_TRANSITIONS, SUBJECT_KINDS, SUBSCRIPTION_MACHINE, SUBSCRIPTION_STATUSES, SUBSCRIPTION_TRANSITIONS, SUPPRESSION_REASONS, SequenceActivated, SequenceArchived, SequenceCreated, SequencePaused, SequenceRepository, StageNotInPipelineError, SubscriptionBounced, SubscriptionComplained, SubscriptionConfirmed, SubscriptionCreated, SubscriptionRepository, SubscriptionUnsubscribed, SuppressionAdded, SuppressionRemoved, SuppressionRepository, UnmanagedSessionError, appendStatus, buildAccountSchema, buildActivitySchema, buildCampaignSchema, buildContactSchema, buildConversationSchema, buildEmailEventSchema, buildEnrollmentSchema, buildLeadSchema, buildLossReasonSchema, buildMessageSchema, buildNoteSchema, buildOpportunitySchema, buildPipelineSchema, buildSequenceSchema, buildSubscriptionSchema, buildSuppressionSchema, canTransitionActivity, canTransitionCampaign, canTransitionEnrollment, canTransitionLead, canTransitionOpportunity, canTransitionSequence, canTransitionSubscription, composeScorers, createAccountModel, createActivityModel, createCampaignModel, createContactModel, createConversationModel, createCrm, createCrmModels, createCrmRepositories, createEmailEventModel, createEvent, createLeadModel, createLossReasonModel, createMessageModel, createNoteModel, createOpportunityModel, createPipelineModel, createSubscriptionModel, crmEventDefinitions, defaultLeadScorer, flushPending, forecast, lastTransitionTo, latestEntry, pipelineConversion, runUnit, tenantFieldDef, timeInStatus, validators_exports as validators };
|