@classytic/crm 0.3.0 → 0.5.1
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 +130 -0
- package/README.md +157 -255
- package/dist/events.d.ts +413 -0
- package/dist/events.js +716 -0
- package/dist/index.d.ts +707 -55
- package/dist/index.js +1667 -113
- package/package.json +14 -10
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,11 +1,104 @@
|
|
|
1
1
|
import { t as __exportAll } from "./chunk.js";
|
|
2
2
|
import { Schema } from "mongoose";
|
|
3
|
-
import { Repository, aggregateHelpersPlugin, methodRegistryPlugin, multiTenantPlugin } from "@classytic/mongokit";
|
|
3
|
+
import { Repository, aggregateHelpersPlugin, isDuplicateKeyError, methodRegistryPlugin, multiTenantPlugin } from "@classytic/mongokit";
|
|
4
4
|
import { createEvent as createEvent$1, matchEventPattern } from "@classytic/primitives/events";
|
|
5
5
|
import { assertAndClaim, defineStateMachine } from "@classytic/primitives/state-machine";
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import { appendStatus, lastTransitionTo, latestEntry, timeInStatus } from "@classytic/primitives/status-history";
|
|
9
|
+
//#region src/core/errors.ts
|
|
10
|
+
/**
|
|
11
|
+
* CRM error vocabulary. Hosts catch `CrmError` at API edges.
|
|
12
|
+
*/
|
|
13
|
+
var CrmError = class extends Error {
|
|
14
|
+
code;
|
|
15
|
+
details;
|
|
16
|
+
constructor(code, message, details) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = "CrmError";
|
|
19
|
+
this.code = code;
|
|
20
|
+
if (details !== void 0) this.details = details;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
var NotFoundError = class extends CrmError {
|
|
24
|
+
constructor(kind, id) {
|
|
25
|
+
super(`${kind.toUpperCase()}_NOT_FOUND`, `${kind} '${id}' not found`, {
|
|
26
|
+
kind,
|
|
27
|
+
id
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
var InvalidLeadTransitionError = class extends CrmError {
|
|
32
|
+
constructor(details) {
|
|
33
|
+
super("INVALID_LEAD_TRANSITION", `Lead ${details.leadId} cannot move from '${details.from}' to '${details.to}'`, details);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
var InvalidOpportunityTransitionError = class extends CrmError {
|
|
37
|
+
constructor(details) {
|
|
38
|
+
super("INVALID_OPPORTUNITY_TRANSITION", `Opportunity ${details.opportunityId} cannot move from '${details.from}' to '${details.to}'`, details);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
var InvalidActivityTransitionError = class extends CrmError {
|
|
42
|
+
constructor(details) {
|
|
43
|
+
super("INVALID_ACTIVITY_TRANSITION", `Activity ${details.activityId} cannot move from '${details.from}' to '${details.to}'`, details);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
var StageNotInPipelineError = class extends CrmError {
|
|
47
|
+
constructor(details) {
|
|
48
|
+
super("STAGE_NOT_IN_PIPELINE", `Stage '${details.stageId}' is not defined on pipeline '${details.pipelineId}'`, details);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
var LeadAlreadyConvertedError = class extends CrmError {
|
|
52
|
+
constructor(leadId) {
|
|
53
|
+
super("LEAD_ALREADY_CONVERTED", `Lead '${leadId}' is already converted`, { leadId });
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
var DuplicateLeadError = class extends CrmError {
|
|
57
|
+
constructor(details) {
|
|
58
|
+
super("DUPLICATE_LEAD", `A lead with this email / phone already exists`, details);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
var CrmModelCollisionError = class extends CrmError {
|
|
62
|
+
constructor(modelName) {
|
|
63
|
+
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 });
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
var EmptyPipelineError = class extends CrmError {
|
|
67
|
+
constructor(pipelineId) {
|
|
68
|
+
super("EMPTY_PIPELINE", `Pipeline '${pipelineId}' has no stages`, { pipelineId });
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
var InvalidCampaignTransitionError = class extends CrmError {
|
|
72
|
+
constructor(details) {
|
|
73
|
+
super("INVALID_CAMPAIGN_TRANSITION", `Campaign ${details.campaignId} cannot move from '${details.from}' to '${details.to}'`, details);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
var DuplicateCampaignError = class extends CrmError {
|
|
77
|
+
constructor(name) {
|
|
78
|
+
super("DUPLICATE_CAMPAIGN", `A campaign named '${name}' already exists`, { name });
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
var InvalidSubscriptionTransitionError = class extends CrmError {
|
|
82
|
+
constructor(details) {
|
|
83
|
+
super("INVALID_SUBSCRIPTION_TRANSITION", `Subscription ${details.subscriptionId} cannot move from '${details.from}' to '${details.to}'`, details);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
var DuplicateSubscriptionError = class extends CrmError {
|
|
87
|
+
constructor(details) {
|
|
88
|
+
super("DUPLICATE_SUBSCRIPTION", `Contact '${details.contactId}' already has a live subscription on campaign '${details.campaignId}'`, details);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
var InvalidConfirmationTokenError = class extends CrmError {
|
|
92
|
+
constructor() {
|
|
93
|
+
super("INVALID_CONFIRMATION_TOKEN", "No subscription matches the supplied confirmation token, or it has already been used");
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
var InvalidUnsubscribeTokenError = class extends CrmError {
|
|
97
|
+
constructor() {
|
|
98
|
+
super("INVALID_UNSUBSCRIBE_TOKEN", "No subscription matches the supplied unsubscribe token");
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
//#endregion
|
|
9
102
|
//#region src/models/tenant-field.ts
|
|
10
103
|
/**
|
|
11
104
|
* Per PACKAGE_RULES §9.2: never hardcode the tenant column's Mongoose
|
|
@@ -272,6 +365,9 @@ function buildLeadSchema(config) {
|
|
|
272
365
|
schema.index({
|
|
273
366
|
organizationId: 1,
|
|
274
367
|
email: 1
|
|
368
|
+
}, {
|
|
369
|
+
unique: true,
|
|
370
|
+
partialFilterExpression: { email: { $type: "string" } }
|
|
275
371
|
});
|
|
276
372
|
schema.index({
|
|
277
373
|
organizationId: 1,
|
|
@@ -495,6 +591,26 @@ const contactInfoSchema$1 = new Schema({
|
|
|
495
591
|
type: String,
|
|
496
592
|
trim: true,
|
|
497
593
|
maxlength: 50
|
|
594
|
+
},
|
|
595
|
+
whatsapp: {
|
|
596
|
+
type: String,
|
|
597
|
+
trim: true,
|
|
598
|
+
maxlength: 50
|
|
599
|
+
},
|
|
600
|
+
telegram: {
|
|
601
|
+
type: String,
|
|
602
|
+
trim: true,
|
|
603
|
+
maxlength: 100
|
|
604
|
+
},
|
|
605
|
+
linkedin: {
|
|
606
|
+
type: String,
|
|
607
|
+
trim: true,
|
|
608
|
+
maxlength: 255
|
|
609
|
+
},
|
|
610
|
+
instagram: {
|
|
611
|
+
type: String,
|
|
612
|
+
trim: true,
|
|
613
|
+
maxlength: 100
|
|
498
614
|
}
|
|
499
615
|
}, { _id: false });
|
|
500
616
|
function buildContactSchema(config) {
|
|
@@ -734,6 +850,10 @@ function buildCampaignSchema(config) {
|
|
|
734
850
|
},
|
|
735
851
|
metadata: { type: Schema.Types.Mixed }
|
|
736
852
|
}, { timestamps: true });
|
|
853
|
+
schema.index({
|
|
854
|
+
organizationId: 1,
|
|
855
|
+
name: 1
|
|
856
|
+
}, { unique: true });
|
|
737
857
|
schema.index({
|
|
738
858
|
organizationId: 1,
|
|
739
859
|
status: 1
|
|
@@ -799,8 +919,14 @@ function canTransitionSubscription(from, to) {
|
|
|
799
919
|
//#endregion
|
|
800
920
|
//#region src/models/subscription.schema.ts
|
|
801
921
|
/**
|
|
802
|
-
*
|
|
803
|
-
* unsubscribe token, compliance audit fields.
|
|
922
|
+
* EmailSubscription — a Contact's opt-in to a Campaign. Double-opt-in
|
|
923
|
+
* tokens, unsubscribe token, compliance audit fields.
|
|
924
|
+
*
|
|
925
|
+
* Registered as Mongoose model `EmailSubscription` (collection
|
|
926
|
+
* `email_subscriptions`) so it doesn't collide with `@classytic/revenue`'s
|
|
927
|
+
* billing-side `Subscription` model when both packages register on the
|
|
928
|
+
* same connection. TypeScript class + file names retain `Subscription` for
|
|
929
|
+
* source-level continuity — only the Mongo identity changed in v0.4.0.
|
|
804
930
|
*/
|
|
805
931
|
const statusEntrySchema = new Schema({
|
|
806
932
|
status: {
|
|
@@ -881,7 +1007,10 @@ function buildSubscriptionSchema(config) {
|
|
|
881
1007
|
},
|
|
882
1008
|
complainedAt: { type: Date },
|
|
883
1009
|
metadata: { type: Schema.Types.Mixed }
|
|
884
|
-
}, {
|
|
1010
|
+
}, {
|
|
1011
|
+
timestamps: true,
|
|
1012
|
+
collection: "email_subscriptions"
|
|
1013
|
+
});
|
|
885
1014
|
schema.index({
|
|
886
1015
|
organizationId: 1,
|
|
887
1016
|
campaignId: 1,
|
|
@@ -891,6 +1020,9 @@ function buildSubscriptionSchema(config) {
|
|
|
891
1020
|
organizationId: 1,
|
|
892
1021
|
contactId: 1,
|
|
893
1022
|
campaignId: 1
|
|
1023
|
+
}, {
|
|
1024
|
+
unique: true,
|
|
1025
|
+
partialFilterExpression: { status: { $in: ["pending", "confirmed"] } }
|
|
894
1026
|
});
|
|
895
1027
|
schema.index({
|
|
896
1028
|
organizationId: 1,
|
|
@@ -900,7 +1032,8 @@ function buildSubscriptionSchema(config) {
|
|
|
900
1032
|
}
|
|
901
1033
|
function createSubscriptionModel(connection, config) {
|
|
902
1034
|
if (connection.models.Subscription) connection.deleteModel("Subscription");
|
|
903
|
-
|
|
1035
|
+
if (connection.models.EmailSubscription) connection.deleteModel("EmailSubscription");
|
|
1036
|
+
return connection.model("EmailSubscription", buildSubscriptionSchema(config));
|
|
904
1037
|
}
|
|
905
1038
|
//#endregion
|
|
906
1039
|
//#region src/enums/activity.ts
|
|
@@ -1255,110 +1388,270 @@ function createEmailEventModel(connection, config) {
|
|
|
1255
1388
|
return connection.model("EmailEvent", buildEmailEventSchema(config));
|
|
1256
1389
|
}
|
|
1257
1390
|
//#endregion
|
|
1258
|
-
//#region src/
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1391
|
+
//#region src/enums/conversation.ts
|
|
1392
|
+
/**
|
|
1393
|
+
* Conversation / Message vocabulary.
|
|
1394
|
+
*
|
|
1395
|
+
* Conversations unify a contact's (or pre-conversion lead's) message
|
|
1396
|
+
* thread across channels (WhatsApp / Telegram / Email / IG-DM / SMS).
|
|
1397
|
+
* The host's webhook adapters write to this surface — the CRM does not
|
|
1398
|
+
* dial outward.
|
|
1399
|
+
*/
|
|
1400
|
+
const CONVERSATION_CHANNELS = [
|
|
1401
|
+
"whatsapp",
|
|
1402
|
+
"telegram",
|
|
1403
|
+
"email",
|
|
1404
|
+
"instagram_dm",
|
|
1405
|
+
"sms",
|
|
1406
|
+
"web_chat"
|
|
1407
|
+
];
|
|
1408
|
+
const CONVERSATION_STATUSES = [
|
|
1409
|
+
"open",
|
|
1410
|
+
"snoozed",
|
|
1411
|
+
"resolved"
|
|
1412
|
+
];
|
|
1413
|
+
/** Conversations attach to either a Contact or a (yet-unconverted) Lead. */
|
|
1414
|
+
const CONVERSATION_PARTICIPANT_KINDS = ["contact", "lead"];
|
|
1415
|
+
const MESSAGE_DIRECTIONS = ["inbound", "outbound"];
|
|
1416
|
+
const MESSAGE_DELIVERY_STATUSES = [
|
|
1417
|
+
"queued",
|
|
1418
|
+
"sent",
|
|
1419
|
+
"delivered",
|
|
1420
|
+
"read",
|
|
1421
|
+
"failed"
|
|
1422
|
+
];
|
|
1423
|
+
//#endregion
|
|
1424
|
+
//#region src/models/conversation.schema.ts
|
|
1425
|
+
/**
|
|
1426
|
+
* Conversation — a multi-channel message thread bound to a Contact or
|
|
1427
|
+
* (pre-conversion) Lead. One row per (channel, participant) pair.
|
|
1428
|
+
*
|
|
1429
|
+
* The CRM stores the thread; channel adapters at the host (WhatsApp Cloud
|
|
1430
|
+
* API, Telegram Bot, IMAP, etc.) write inbound `Message`s and emit
|
|
1431
|
+
* `outbound` requests via the host's bridge — this package never reaches
|
|
1432
|
+
* out to a transport itself (PACKAGE_RULES §11).
|
|
1433
|
+
*/
|
|
1434
|
+
function buildConversationSchema(config) {
|
|
1435
|
+
const schema = new Schema({
|
|
1436
|
+
organizationId: tenantFieldDef(config),
|
|
1437
|
+
channel: {
|
|
1438
|
+
type: String,
|
|
1439
|
+
enum: CONVERSATION_CHANNELS,
|
|
1440
|
+
required: true,
|
|
1441
|
+
index: true
|
|
1442
|
+
},
|
|
1443
|
+
participantKind: {
|
|
1444
|
+
type: String,
|
|
1445
|
+
enum: CONVERSATION_PARTICIPANT_KINDS,
|
|
1446
|
+
required: true
|
|
1447
|
+
},
|
|
1448
|
+
participantId: {
|
|
1449
|
+
type: Schema.Types.ObjectId,
|
|
1450
|
+
required: true,
|
|
1451
|
+
index: true
|
|
1452
|
+
},
|
|
1453
|
+
channelUserId: {
|
|
1454
|
+
type: String,
|
|
1455
|
+
trim: true,
|
|
1456
|
+
maxlength: 255
|
|
1457
|
+
},
|
|
1458
|
+
status: {
|
|
1459
|
+
type: String,
|
|
1460
|
+
enum: CONVERSATION_STATUSES,
|
|
1461
|
+
default: "open",
|
|
1462
|
+
index: true
|
|
1463
|
+
},
|
|
1464
|
+
lastMessageAt: {
|
|
1465
|
+
type: Date,
|
|
1466
|
+
index: true
|
|
1467
|
+
},
|
|
1468
|
+
unreadCount: {
|
|
1469
|
+
type: Number,
|
|
1470
|
+
default: 0,
|
|
1471
|
+
min: 0
|
|
1472
|
+
},
|
|
1473
|
+
assigneeId: {
|
|
1474
|
+
type: String,
|
|
1475
|
+
index: true
|
|
1476
|
+
},
|
|
1477
|
+
metadata: { type: Schema.Types.Mixed }
|
|
1478
|
+
}, { timestamps: true });
|
|
1479
|
+
schema.index({
|
|
1480
|
+
organizationId: 1,
|
|
1481
|
+
participantKind: 1,
|
|
1482
|
+
participantId: 1
|
|
1483
|
+
});
|
|
1484
|
+
schema.index({
|
|
1485
|
+
organizationId: 1,
|
|
1486
|
+
status: 1,
|
|
1487
|
+
lastMessageAt: -1
|
|
1488
|
+
});
|
|
1489
|
+
schema.index({
|
|
1490
|
+
organizationId: 1,
|
|
1491
|
+
assigneeId: 1,
|
|
1492
|
+
status: 1
|
|
1493
|
+
});
|
|
1494
|
+
schema.index({
|
|
1495
|
+
organizationId: 1,
|
|
1496
|
+
channel: 1,
|
|
1497
|
+
channelUserId: 1
|
|
1272
1498
|
});
|
|
1499
|
+
return schema;
|
|
1500
|
+
}
|
|
1501
|
+
function createConversationModel(connection, config) {
|
|
1502
|
+
if (connection.models.Conversation) connection.deleteModel("Conversation");
|
|
1503
|
+
return connection.model("Conversation", buildConversationSchema(config));
|
|
1273
1504
|
}
|
|
1274
1505
|
//#endregion
|
|
1275
|
-
//#region src/
|
|
1506
|
+
//#region src/models/message.schema.ts
|
|
1276
1507
|
/**
|
|
1277
|
-
*
|
|
1508
|
+
* Message — one event inside a Conversation.
|
|
1509
|
+
*
|
|
1510
|
+
* Channel-agnostic: `body` is plain text + optional attachments[].
|
|
1511
|
+
* Channel-specific quirks (WhatsApp template params, IMAP headers,
|
|
1512
|
+
* Telegram entities) live under `metadata`.
|
|
1513
|
+
*
|
|
1514
|
+
* `externalMessageId` is the dedup key webhooks check against before
|
|
1515
|
+
* appending — at-least-once delivery is the rule, not the exception.
|
|
1278
1516
|
*/
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
};
|
|
1302
|
-
var InvalidOpportunityTransitionError = class extends CrmError {
|
|
1303
|
-
constructor(details) {
|
|
1304
|
-
super("INVALID_OPPORTUNITY_TRANSITION", `Opportunity ${details.opportunityId} cannot move from '${details.from}' to '${details.to}'`, details);
|
|
1305
|
-
}
|
|
1306
|
-
};
|
|
1307
|
-
var InvalidActivityTransitionError = class extends CrmError {
|
|
1308
|
-
constructor(details) {
|
|
1309
|
-
super("INVALID_ACTIVITY_TRANSITION", `Activity ${details.activityId} cannot move from '${details.from}' to '${details.to}'`, details);
|
|
1310
|
-
}
|
|
1311
|
-
};
|
|
1312
|
-
var StageNotInPipelineError = class extends CrmError {
|
|
1313
|
-
constructor(details) {
|
|
1314
|
-
super("STAGE_NOT_IN_PIPELINE", `Stage '${details.stageId}' is not defined on pipeline '${details.pipelineId}'`, details);
|
|
1315
|
-
}
|
|
1316
|
-
};
|
|
1317
|
-
var LeadAlreadyConvertedError = class extends CrmError {
|
|
1318
|
-
constructor(leadId) {
|
|
1319
|
-
super("LEAD_ALREADY_CONVERTED", `Lead '${leadId}' is already converted`, { leadId });
|
|
1320
|
-
}
|
|
1321
|
-
};
|
|
1322
|
-
var DuplicateLeadError = class extends CrmError {
|
|
1323
|
-
constructor(details) {
|
|
1324
|
-
super("DUPLICATE_LEAD", `A lead with this email / phone already exists`, details);
|
|
1325
|
-
}
|
|
1326
|
-
};
|
|
1327
|
-
var EmptyPipelineError = class extends CrmError {
|
|
1328
|
-
constructor(pipelineId) {
|
|
1329
|
-
super("EMPTY_PIPELINE", `Pipeline '${pipelineId}' has no stages`, { pipelineId });
|
|
1330
|
-
}
|
|
1331
|
-
};
|
|
1332
|
-
var InvalidCampaignTransitionError = class extends CrmError {
|
|
1333
|
-
constructor(details) {
|
|
1334
|
-
super("INVALID_CAMPAIGN_TRANSITION", `Campaign ${details.campaignId} cannot move from '${details.from}' to '${details.to}'`, details);
|
|
1517
|
+
const attachmentSchema = new Schema({
|
|
1518
|
+
type: {
|
|
1519
|
+
type: String,
|
|
1520
|
+
required: true,
|
|
1521
|
+
maxlength: 50
|
|
1522
|
+
},
|
|
1523
|
+
url: {
|
|
1524
|
+
type: String,
|
|
1525
|
+
required: true,
|
|
1526
|
+
maxlength: 2048
|
|
1527
|
+
},
|
|
1528
|
+
name: {
|
|
1529
|
+
type: String,
|
|
1530
|
+
maxlength: 255
|
|
1531
|
+
},
|
|
1532
|
+
size: {
|
|
1533
|
+
type: Number,
|
|
1534
|
+
min: 0
|
|
1535
|
+
},
|
|
1536
|
+
mimeType: {
|
|
1537
|
+
type: String,
|
|
1538
|
+
maxlength: 150
|
|
1335
1539
|
}
|
|
1336
|
-
};
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1540
|
+
}, { _id: false });
|
|
1541
|
+
function buildMessageSchema(config) {
|
|
1542
|
+
const schema = new Schema({
|
|
1543
|
+
organizationId: tenantFieldDef(config),
|
|
1544
|
+
conversationId: {
|
|
1545
|
+
type: Schema.Types.ObjectId,
|
|
1546
|
+
ref: "Conversation",
|
|
1547
|
+
required: true,
|
|
1548
|
+
index: true
|
|
1549
|
+
},
|
|
1550
|
+
direction: {
|
|
1551
|
+
type: String,
|
|
1552
|
+
enum: MESSAGE_DIRECTIONS,
|
|
1553
|
+
required: true
|
|
1554
|
+
},
|
|
1555
|
+
body: {
|
|
1556
|
+
type: String,
|
|
1557
|
+
default: "",
|
|
1558
|
+
maxlength: 16384
|
|
1559
|
+
},
|
|
1560
|
+
attachments: {
|
|
1561
|
+
type: [attachmentSchema],
|
|
1562
|
+
default: []
|
|
1563
|
+
},
|
|
1564
|
+
externalMessageId: {
|
|
1565
|
+
type: String,
|
|
1566
|
+
trim: true,
|
|
1567
|
+
maxlength: 255
|
|
1568
|
+
},
|
|
1569
|
+
status: {
|
|
1570
|
+
type: String,
|
|
1571
|
+
enum: MESSAGE_DELIVERY_STATUSES,
|
|
1572
|
+
default: "sent"
|
|
1573
|
+
},
|
|
1574
|
+
senderRef: {
|
|
1575
|
+
type: String,
|
|
1576
|
+
trim: true,
|
|
1577
|
+
maxlength: 255
|
|
1578
|
+
},
|
|
1579
|
+
recipientRef: {
|
|
1580
|
+
type: String,
|
|
1581
|
+
trim: true,
|
|
1582
|
+
maxlength: 255
|
|
1583
|
+
},
|
|
1584
|
+
sentAt: { type: Date },
|
|
1585
|
+
metadata: { type: Schema.Types.Mixed }
|
|
1586
|
+
}, { timestamps: true });
|
|
1587
|
+
schema.index({
|
|
1588
|
+
organizationId: 1,
|
|
1589
|
+
conversationId: 1,
|
|
1590
|
+
createdAt: -1
|
|
1591
|
+
});
|
|
1592
|
+
schema.index({
|
|
1593
|
+
organizationId: 1,
|
|
1594
|
+
externalMessageId: 1
|
|
1595
|
+
}, {
|
|
1596
|
+
unique: true,
|
|
1597
|
+
partialFilterExpression: { externalMessageId: { $type: "string" } }
|
|
1598
|
+
});
|
|
1599
|
+
schema.index({ sentAt: 1 }, {
|
|
1600
|
+
expireAfterSeconds: 365 * 24 * 60 * 60,
|
|
1601
|
+
sparse: true,
|
|
1602
|
+
name: "sentAt_ttl"
|
|
1603
|
+
});
|
|
1604
|
+
return schema;
|
|
1605
|
+
}
|
|
1606
|
+
function createMessageModel(connection, config) {
|
|
1607
|
+
if (connection.models.Message) connection.deleteModel("Message");
|
|
1608
|
+
return connection.model("Message", buildMessageSchema(config));
|
|
1609
|
+
}
|
|
1610
|
+
//#endregion
|
|
1611
|
+
//#region src/models/create-models.ts
|
|
1612
|
+
/** Mongoose model names this package registers — drives the §21 collision check. */
|
|
1613
|
+
const CRM_MODEL_NAMES = [
|
|
1614
|
+
"Pipeline",
|
|
1615
|
+
"Lead",
|
|
1616
|
+
"Opportunity",
|
|
1617
|
+
"Contact",
|
|
1618
|
+
"Account",
|
|
1619
|
+
"Campaign",
|
|
1620
|
+
"EmailSubscription",
|
|
1621
|
+
"Activity",
|
|
1622
|
+
"Note",
|
|
1623
|
+
"LossReason",
|
|
1624
|
+
"EmailEvent",
|
|
1625
|
+
"Conversation",
|
|
1626
|
+
"Message"
|
|
1627
|
+
];
|
|
1628
|
+
function createCrmModels(connection, config) {
|
|
1629
|
+
const forceRecreate = config.forceRecreate ?? false;
|
|
1630
|
+
const autoIndex = config.autoIndex ?? true;
|
|
1631
|
+
const namesToCheck = [...CRM_MODEL_NAMES, "Subscription"];
|
|
1632
|
+
if (forceRecreate) {
|
|
1633
|
+
for (const name of namesToCheck) if (connection.models[name]) connection.deleteModel(name);
|
|
1634
|
+
} else for (const name of CRM_MODEL_NAMES) if (connection.models[name]) throw new CrmModelCollisionError(name);
|
|
1635
|
+
const register = (name, schema) => {
|
|
1636
|
+
schema.set("autoIndex", autoIndex);
|
|
1637
|
+
return connection.model(name, schema);
|
|
1638
|
+
};
|
|
1639
|
+
return Object.freeze({
|
|
1640
|
+
Pipeline: register("Pipeline", buildPipelineSchema(config)),
|
|
1641
|
+
Lead: register("Lead", buildLeadSchema(config)),
|
|
1642
|
+
Opportunity: register("Opportunity", buildOpportunitySchema(config)),
|
|
1643
|
+
Contact: register("Contact", buildContactSchema(config)),
|
|
1644
|
+
Account: register("Account", buildAccountSchema(config)),
|
|
1645
|
+
Campaign: register("Campaign", buildCampaignSchema(config)),
|
|
1646
|
+
Subscription: register("EmailSubscription", buildSubscriptionSchema(config)),
|
|
1647
|
+
Activity: register("Activity", buildActivitySchema(config)),
|
|
1648
|
+
Note: register("Note", buildNoteSchema(config)),
|
|
1649
|
+
LossReason: register("LossReason", buildLossReasonSchema(config)),
|
|
1650
|
+
EmailEvent: register("EmailEvent", buildEmailEventSchema(config)),
|
|
1651
|
+
Conversation: register("Conversation", buildConversationSchema(config)),
|
|
1652
|
+
Message: register("Message", buildMessageSchema(config))
|
|
1653
|
+
});
|
|
1654
|
+
}
|
|
1362
1655
|
//#endregion
|
|
1363
1656
|
//#region src/events/event-constants.ts
|
|
1364
1657
|
/** PACKAGE_RULES rule 12: `package:resource.verb`. */
|
|
@@ -1399,7 +1692,26 @@ const CRM_EVENTS = {
|
|
|
1399
1692
|
EMAIL_EVENT_CLICKED: "crm:email_event.clicked",
|
|
1400
1693
|
EMAIL_EVENT_BOUNCED: "crm:email_event.bounced",
|
|
1401
1694
|
EMAIL_EVENT_COMPLAINED: "crm:email_event.complained",
|
|
1402
|
-
EMAIL_EVENT_UNSUBSCRIBED: "crm:email_event.unsubscribed"
|
|
1695
|
+
EMAIL_EVENT_UNSUBSCRIBED: "crm:email_event.unsubscribed",
|
|
1696
|
+
LEAD_ENRICHED: "crm:lead.enriched",
|
|
1697
|
+
LEAD_ASSIGNED: "crm:lead.assigned",
|
|
1698
|
+
CONTACT_ENRICHED: "crm:contact.enriched",
|
|
1699
|
+
CONTACT_ASSIGNED: "crm:contact.assigned",
|
|
1700
|
+
CONTACT_MERGED: "crm:contact.merged",
|
|
1701
|
+
ACCOUNT_ENRICHED: "crm:account.enriched",
|
|
1702
|
+
ACCOUNT_ASSIGNED: "crm:account.assigned",
|
|
1703
|
+
ACCOUNT_MERGED: "crm:account.merged",
|
|
1704
|
+
OPPORTUNITY_ASSIGNED: "crm:opportunity.assigned",
|
|
1705
|
+
OPPORTUNITY_AMOUNT_CHANGED: "crm:opportunity.amount_changed",
|
|
1706
|
+
OPPORTUNITY_EXTENDED: "crm:opportunity.extended",
|
|
1707
|
+
ACTIVITY_RESCHEDULED: "crm:activity.rescheduled",
|
|
1708
|
+
ACTIVITY_OUTCOME_RECORDED: "crm:activity.outcome_recorded",
|
|
1709
|
+
CONVERSATION_CREATED: "crm:conversation.created",
|
|
1710
|
+
CONVERSATION_ASSIGNED: "crm:conversation.assigned",
|
|
1711
|
+
CONVERSATION_RESOLVED: "crm:conversation.resolved",
|
|
1712
|
+
CONVERSATION_REOPENED: "crm:conversation.reopened",
|
|
1713
|
+
MESSAGE_RECEIVED: "crm:message.received",
|
|
1714
|
+
MESSAGE_SENT: "crm:message.sent"
|
|
1403
1715
|
};
|
|
1404
1716
|
//#endregion
|
|
1405
1717
|
//#region src/events/helpers.ts
|
|
@@ -1582,7 +1894,6 @@ var LeadRepository = class extends Repository {
|
|
|
1582
1894
|
const computedFullName = input.fullName ?? ([input.firstName, input.lastName].filter(Boolean).join(" ").trim() || input.email || input.phone);
|
|
1583
1895
|
const draft = {
|
|
1584
1896
|
...input,
|
|
1585
|
-
fullName: computedFullName,
|
|
1586
1897
|
status: input.status ?? "new",
|
|
1587
1898
|
score: 0,
|
|
1588
1899
|
statusHistory: [{
|
|
@@ -1590,7 +1901,14 @@ var LeadRepository = class extends Repository {
|
|
|
1590
1901
|
occurredAt: now
|
|
1591
1902
|
}]
|
|
1592
1903
|
};
|
|
1593
|
-
|
|
1904
|
+
if (computedFullName !== void 0) draft.fullName = computedFullName;
|
|
1905
|
+
let created;
|
|
1906
|
+
try {
|
|
1907
|
+
created = await this.create(draft, ctx);
|
|
1908
|
+
} catch (err) {
|
|
1909
|
+
if (this.rejectDuplicates && input.email && isDuplicateKeyError(err)) throw new DuplicateLeadError({ email: input.email });
|
|
1910
|
+
throw err;
|
|
1911
|
+
}
|
|
1594
1912
|
const score = this.scorer(created);
|
|
1595
1913
|
let result = created;
|
|
1596
1914
|
if (score !== 0) {
|
|
@@ -1629,6 +1947,50 @@ var LeadRepository = class extends Repository {
|
|
|
1629
1947
|
return this._transition(id, "nurturing", extras, ctx);
|
|
1630
1948
|
}
|
|
1631
1949
|
/**
|
|
1950
|
+
* Merge enrichment payload (3rd-party provider response or manual
|
|
1951
|
+
* patch) into the lead doc, then re-score. Emits `crm:lead.enriched`
|
|
1952
|
+
* with the field set written so subscribers can fan out (e.g. push a
|
|
1953
|
+
* `recompute_segment` job) without re-reading the doc.
|
|
1954
|
+
*
|
|
1955
|
+
* The `provider` tag flows into the event for attribution; the package
|
|
1956
|
+
* is provider-agnostic so the actual enrichment call lives at the host.
|
|
1957
|
+
*/
|
|
1958
|
+
async enrich(id, patch, extras = {}, ctx) {
|
|
1959
|
+
if (!await this.getById(id, ctx)) throw new NotFoundError("Lead", id);
|
|
1960
|
+
if (!await this.update(id, patch, ctx)) throw new NotFoundError("Lead", id);
|
|
1961
|
+
const fields = Object.keys(patch);
|
|
1962
|
+
await this.events.publish(createEvent(CRM_EVENTS.LEAD_ENRICHED, {
|
|
1963
|
+
leadId: id,
|
|
1964
|
+
fields,
|
|
1965
|
+
...extras.provider !== void 0 ? { provider: extras.provider } : {}
|
|
1966
|
+
}, ctx));
|
|
1967
|
+
return this.recomputeScore(id, ctx);
|
|
1968
|
+
}
|
|
1969
|
+
/**
|
|
1970
|
+
* Assign / re-assign the lead's owner. Emits `crm:lead.assigned` with
|
|
1971
|
+
* the previous owner so audit consumers can compute hand-off chains.
|
|
1972
|
+
*/
|
|
1973
|
+
async assign(id, ownerId, ctx) {
|
|
1974
|
+
const before = await this.findOneAndUpdate({
|
|
1975
|
+
_id: id,
|
|
1976
|
+
ownerId: { $ne: ownerId }
|
|
1977
|
+
}, { $set: { ownerId } }, {
|
|
1978
|
+
...ctx,
|
|
1979
|
+
returnDocument: "before"
|
|
1980
|
+
});
|
|
1981
|
+
if (!before) {
|
|
1982
|
+
const current = await this.getById(id, ctx);
|
|
1983
|
+
if (!current) throw new NotFoundError("Lead", id);
|
|
1984
|
+
return current;
|
|
1985
|
+
}
|
|
1986
|
+
await this.events.publish(createEvent(CRM_EVENTS.LEAD_ASSIGNED, {
|
|
1987
|
+
leadId: id,
|
|
1988
|
+
ownerId,
|
|
1989
|
+
...before.ownerId !== void 0 ? { previousOwnerId: before.ownerId } : {}
|
|
1990
|
+
}, ctx));
|
|
1991
|
+
return await this.getById(id, ctx) ?? before;
|
|
1992
|
+
}
|
|
1993
|
+
/**
|
|
1632
1994
|
* Recompute + persist the lead's score. Called from `createLead`
|
|
1633
1995
|
* automatically; hosts also call this after enrichment writes.
|
|
1634
1996
|
*/
|
|
@@ -1676,6 +2038,29 @@ var LeadRepository = class extends Repository {
|
|
|
1676
2038
|
}, ctx));
|
|
1677
2039
|
return updated;
|
|
1678
2040
|
}
|
|
2041
|
+
/**
|
|
2042
|
+
* Count leads created on or after `since`. Used by dashboard KPI tiles.
|
|
2043
|
+
* Goes through the inherited tenant-scoped `count()` — the plugin
|
|
2044
|
+
* injects `organizationId` (never a manual `if (ctx)` filter, which
|
|
2045
|
+
* silently reads ALL tenants when ctx is absent).
|
|
2046
|
+
*/
|
|
2047
|
+
async countCreatedSince(since, ctx) {
|
|
2048
|
+
return this.count({ createdAt: { $gte: since } }, ctx);
|
|
2049
|
+
}
|
|
2050
|
+
/** Latest `limit` leads — lightweight projection for the activity feed. */
|
|
2051
|
+
async findRecentForFeed(limit, ctx) {
|
|
2052
|
+
return this.findAll({}, {
|
|
2053
|
+
...ctx,
|
|
2054
|
+
sort: { updatedAt: -1 },
|
|
2055
|
+
limit,
|
|
2056
|
+
select: {
|
|
2057
|
+
_id: 1,
|
|
2058
|
+
fullName: 1,
|
|
2059
|
+
status: 1,
|
|
2060
|
+
updatedAt: 1
|
|
2061
|
+
}
|
|
2062
|
+
});
|
|
2063
|
+
}
|
|
1679
2064
|
};
|
|
1680
2065
|
//#endregion
|
|
1681
2066
|
//#region src/repositories/opportunity.repository.ts
|
|
@@ -1799,6 +2184,83 @@ var OpportunityRepository = class extends Repository {
|
|
|
1799
2184
|
}, ctx));
|
|
1800
2185
|
return updated;
|
|
1801
2186
|
}
|
|
2187
|
+
/**
|
|
2188
|
+
* Re-assign opportunity owner. Mutates `ownerId` only; no status
|
|
2189
|
+
* transition. Emits `crm:opportunity.assigned` with the previous owner.
|
|
2190
|
+
*/
|
|
2191
|
+
async assign(id, ownerId, ctx) {
|
|
2192
|
+
const before = await this.findOneAndUpdate({
|
|
2193
|
+
_id: id,
|
|
2194
|
+
ownerId: { $ne: ownerId }
|
|
2195
|
+
}, { $set: { ownerId } }, {
|
|
2196
|
+
...ctx,
|
|
2197
|
+
returnDocument: "before"
|
|
2198
|
+
});
|
|
2199
|
+
if (!before) {
|
|
2200
|
+
const current = await this.getById(id, ctx);
|
|
2201
|
+
if (!current) throw new NotFoundError("Opportunity", id);
|
|
2202
|
+
return current;
|
|
2203
|
+
}
|
|
2204
|
+
await this.events.publish(createEvent(CRM_EVENTS.OPPORTUNITY_ASSIGNED, {
|
|
2205
|
+
opportunityId: id,
|
|
2206
|
+
ownerId,
|
|
2207
|
+
...before.ownerId !== void 0 ? { previousOwnerId: before.ownerId } : {}
|
|
2208
|
+
}, ctx));
|
|
2209
|
+
return await this.getById(id, ctx) ?? before;
|
|
2210
|
+
}
|
|
2211
|
+
/**
|
|
2212
|
+
* Adjust the deal amount on an open opportunity. Rejected after
|
|
2213
|
+
* win/lose/abandon — closed-deal amounts are immutable history.
|
|
2214
|
+
* Emits `crm:opportunity.amount_changed` with the delta so forecast
|
|
2215
|
+
* subscribers can re-bucket.
|
|
2216
|
+
*/
|
|
2217
|
+
async updateAmount(id, amount, extras = {}, ctx) {
|
|
2218
|
+
const existing = await this.getById(id, ctx);
|
|
2219
|
+
if (!existing) throw new NotFoundError("Opportunity", id);
|
|
2220
|
+
if (existing.status !== "open") throw new InvalidOpportunityTransitionError({
|
|
2221
|
+
from: existing.status,
|
|
2222
|
+
to: existing.status,
|
|
2223
|
+
opportunityId: id
|
|
2224
|
+
});
|
|
2225
|
+
if (amount === existing.amount) return existing;
|
|
2226
|
+
const updated = await this.update(id, { amount }, ctx);
|
|
2227
|
+
if (!updated) throw new NotFoundError("Opportunity", id);
|
|
2228
|
+
await this.events.publish(createEvent(CRM_EVENTS.OPPORTUNITY_AMOUNT_CHANGED, {
|
|
2229
|
+
opportunityId: id,
|
|
2230
|
+
amount,
|
|
2231
|
+
...existing.amount !== void 0 ? { previousAmount: existing.amount } : {},
|
|
2232
|
+
...extras.reason !== void 0 ? { reason: extras.reason } : {},
|
|
2233
|
+
...extras.by !== void 0 ? { by: extras.by } : {}
|
|
2234
|
+
}, ctx));
|
|
2235
|
+
return updated;
|
|
2236
|
+
}
|
|
2237
|
+
/**
|
|
2238
|
+
* Push `expectedCloseAt` further out. Only valid on open opps —
|
|
2239
|
+
* extending a closed deal is meaningless. The new date must be later
|
|
2240
|
+
* than the current one; pulling the date in is silently accepted
|
|
2241
|
+
* (no event is meaningful there).
|
|
2242
|
+
*/
|
|
2243
|
+
async extendCloseDate(id, newDate, extras = {}, ctx) {
|
|
2244
|
+
const existing = await this.getById(id, ctx);
|
|
2245
|
+
if (!existing) throw new NotFoundError("Opportunity", id);
|
|
2246
|
+
if (existing.status !== "open") throw new InvalidOpportunityTransitionError({
|
|
2247
|
+
from: existing.status,
|
|
2248
|
+
to: existing.status,
|
|
2249
|
+
opportunityId: id
|
|
2250
|
+
});
|
|
2251
|
+
const previous = existing.expectedCloseAt;
|
|
2252
|
+
if (previous && newDate <= previous) return existing;
|
|
2253
|
+
const updated = await this.update(id, { expectedCloseAt: newDate }, ctx);
|
|
2254
|
+
if (!updated) throw new NotFoundError("Opportunity", id);
|
|
2255
|
+
await this.events.publish(createEvent(CRM_EVENTS.OPPORTUNITY_EXTENDED, {
|
|
2256
|
+
opportunityId: id,
|
|
2257
|
+
expectedCloseAt: newDate.toISOString(),
|
|
2258
|
+
...previous ? { previousExpectedCloseAt: previous.toISOString() } : {},
|
|
2259
|
+
...extras.reason !== void 0 ? { reason: extras.reason } : {},
|
|
2260
|
+
...extras.by !== void 0 ? { by: extras.by } : {}
|
|
2261
|
+
}, ctx));
|
|
2262
|
+
return updated;
|
|
2263
|
+
}
|
|
1802
2264
|
async abandon(id, by, note, ctx) {
|
|
1803
2265
|
const updated = await this._closeStatus(id, "abandoned", {
|
|
1804
2266
|
closedAt: /* @__PURE__ */ new Date(),
|
|
@@ -1837,6 +2299,58 @@ var OpportunityRepository = class extends Repository {
|
|
|
1837
2299
|
});
|
|
1838
2300
|
return updated;
|
|
1839
2301
|
}
|
|
2302
|
+
/** Count opportunities with status `open`. Used by dashboard KPI tiles. */
|
|
2303
|
+
async countOpen(ctx) {
|
|
2304
|
+
return this.count({ status: "open" }, ctx);
|
|
2305
|
+
}
|
|
2306
|
+
/**
|
|
2307
|
+
* Sum `amount` on won opportunities updated within `since`.
|
|
2308
|
+
* Goes through the tenant-scoped `findAll()` — the multiTenant plugin
|
|
2309
|
+
* injects `organizationId`, so a missing ctx can't leak other tenants'
|
|
2310
|
+
* revenue (a manual `if (ctx)` filter would).
|
|
2311
|
+
*/
|
|
2312
|
+
async sumWonSince(since, ctx) {
|
|
2313
|
+
const rows = await this.findAll({
|
|
2314
|
+
status: "won",
|
|
2315
|
+
updatedAt: { $gte: since }
|
|
2316
|
+
}, {
|
|
2317
|
+
...ctx,
|
|
2318
|
+
select: {
|
|
2319
|
+
amount: 1,
|
|
2320
|
+
currency: 1
|
|
2321
|
+
}
|
|
2322
|
+
});
|
|
2323
|
+
if (rows.length === 0) return null;
|
|
2324
|
+
const byCurrency = /* @__PURE__ */ new Map();
|
|
2325
|
+
for (const r of rows) {
|
|
2326
|
+
const cur = r.currency ?? "UNKNOWN";
|
|
2327
|
+
byCurrency.set(cur, (byCurrency.get(cur) ?? 0) + (r.amount ?? 0));
|
|
2328
|
+
}
|
|
2329
|
+
let topCurrency = "";
|
|
2330
|
+
let topTotal = -Infinity;
|
|
2331
|
+
for (const [cur, total] of byCurrency) if (total > topTotal) {
|
|
2332
|
+
topTotal = total;
|
|
2333
|
+
topCurrency = cur;
|
|
2334
|
+
}
|
|
2335
|
+
return {
|
|
2336
|
+
total: topTotal,
|
|
2337
|
+
currency: topCurrency
|
|
2338
|
+
};
|
|
2339
|
+
}
|
|
2340
|
+
/** Latest `limit` opportunities — lightweight projection for the activity feed. */
|
|
2341
|
+
async findRecentForFeed(limit, ctx) {
|
|
2342
|
+
return this.findAll({}, {
|
|
2343
|
+
...ctx,
|
|
2344
|
+
sort: { updatedAt: -1 },
|
|
2345
|
+
limit,
|
|
2346
|
+
select: {
|
|
2347
|
+
_id: 1,
|
|
2348
|
+
name: 1,
|
|
2349
|
+
status: 1,
|
|
2350
|
+
updatedAt: 1
|
|
2351
|
+
}
|
|
2352
|
+
});
|
|
2353
|
+
}
|
|
1840
2354
|
};
|
|
1841
2355
|
//#endregion
|
|
1842
2356
|
//#region src/repositories/contact.repository.ts
|
|
@@ -1858,7 +2372,108 @@ var ContactRepository = class extends Repository {
|
|
|
1858
2372
|
}, ctx));
|
|
1859
2373
|
return created;
|
|
1860
2374
|
}
|
|
2375
|
+
/**
|
|
2376
|
+
* Merge enrichment payload into the contact. The `patch` is applied
|
|
2377
|
+
* verbatim; deep-merge of `contact.*` channels is the caller's job
|
|
2378
|
+
* (they own the provider response shape).
|
|
2379
|
+
*/
|
|
2380
|
+
async enrich(id, patch, extras = {}, ctx) {
|
|
2381
|
+
if (!await this.getById(id, ctx)) throw new NotFoundError("Contact", id);
|
|
2382
|
+
const updated = await this.update(id, patch, ctx);
|
|
2383
|
+
if (!updated) throw new NotFoundError("Contact", id);
|
|
2384
|
+
await this.events.publish(createEvent(CRM_EVENTS.CONTACT_ENRICHED, {
|
|
2385
|
+
contactId: id,
|
|
2386
|
+
fields: Object.keys(patch),
|
|
2387
|
+
...extras.provider !== void 0 ? { provider: extras.provider } : {}
|
|
2388
|
+
}, ctx));
|
|
2389
|
+
return updated;
|
|
2390
|
+
}
|
|
2391
|
+
/** Set / re-assign the contact's owner. Atomic; no-op when unchanged. */
|
|
2392
|
+
async assign(id, ownerId, ctx) {
|
|
2393
|
+
const before = await this.findOneAndUpdate({
|
|
2394
|
+
_id: id,
|
|
2395
|
+
ownerId: { $ne: ownerId }
|
|
2396
|
+
}, { $set: { ownerId } }, {
|
|
2397
|
+
...ctx,
|
|
2398
|
+
returnDocument: "before"
|
|
2399
|
+
});
|
|
2400
|
+
if (!before) {
|
|
2401
|
+
const current = await this.getById(id, ctx);
|
|
2402
|
+
if (!current) throw new NotFoundError("Contact", id);
|
|
2403
|
+
return current;
|
|
2404
|
+
}
|
|
2405
|
+
await this.events.publish(createEvent(CRM_EVENTS.CONTACT_ASSIGNED, {
|
|
2406
|
+
contactId: id,
|
|
2407
|
+
ownerId,
|
|
2408
|
+
...before.ownerId !== void 0 ? { previousOwnerId: before.ownerId } : {}
|
|
2409
|
+
}, ctx));
|
|
2410
|
+
return await this.getById(id, ctx) ?? before;
|
|
2411
|
+
}
|
|
2412
|
+
/**
|
|
2413
|
+
* Merge two contacts. Secondary's non-conflicting fields are folded
|
|
2414
|
+
* into primary, tags are unioned, then secondary is deleted.
|
|
2415
|
+
*
|
|
2416
|
+
* Hosts wanting transactional safety can pass `ctx.session` —
|
|
2417
|
+
* mongokit threads it through `update` + `delete`.
|
|
2418
|
+
*
|
|
2419
|
+
* Re-pointing related Activities / Notes / Opportunities is the host's
|
|
2420
|
+
* job (they own those relationships) — subscribe to
|
|
2421
|
+
* `crm:contact.merged` and rewrite `subjectId` / `primaryContactId`
|
|
2422
|
+
* from there.
|
|
2423
|
+
*/
|
|
2424
|
+
async merge(input, ctx) {
|
|
2425
|
+
const { primaryId, secondaryId } = input;
|
|
2426
|
+
if (primaryId === secondaryId) throw new Error("[crm] cannot merge a contact with itself");
|
|
2427
|
+
const strategy = input.strategy ?? "preferPrimary";
|
|
2428
|
+
const [primary, secondary] = await Promise.all([this.getById(primaryId, ctx), this.getById(secondaryId, ctx)]);
|
|
2429
|
+
if (!primary) throw new NotFoundError("Contact", primaryId);
|
|
2430
|
+
if (!secondary) throw new NotFoundError("Contact", secondaryId);
|
|
2431
|
+
const winner = strategy === "preferPrimary" ? primary : secondary;
|
|
2432
|
+
const loser = strategy === "preferPrimary" ? secondary : primary;
|
|
2433
|
+
const winnerContact = plainOf(winner.contact);
|
|
2434
|
+
const loserContact = plainOf(loser.contact);
|
|
2435
|
+
const winnerName = plainOf(winner.name);
|
|
2436
|
+
const loserName = plainOf(loser.name);
|
|
2437
|
+
const mergedContactInfo = {
|
|
2438
|
+
...loserContact,
|
|
2439
|
+
...stripEmpty(winnerContact)
|
|
2440
|
+
};
|
|
2441
|
+
const patch = {
|
|
2442
|
+
name: {
|
|
2443
|
+
...loserName,
|
|
2444
|
+
...stripEmpty(winnerName)
|
|
2445
|
+
},
|
|
2446
|
+
contact: mergedContactInfo,
|
|
2447
|
+
tags: Array.from(new Set([...primary.tags ?? [], ...secondary.tags ?? []])),
|
|
2448
|
+
...winner.jobTitle !== void 0 ? { jobTitle: winner.jobTitle } : loser.jobTitle !== void 0 ? { jobTitle: loser.jobTitle } : {},
|
|
2449
|
+
...winner.accountId !== void 0 ? { accountId: winner.accountId } : loser.accountId !== void 0 ? { accountId: loser.accountId } : {},
|
|
2450
|
+
...winner.ownerId !== void 0 ? { ownerId: winner.ownerId } : loser.ownerId !== void 0 ? { ownerId: loser.ownerId } : {}
|
|
2451
|
+
};
|
|
2452
|
+
const updated = await this.update(primaryId, patch, ctx);
|
|
2453
|
+
if (!updated) throw new NotFoundError("Contact", primaryId);
|
|
2454
|
+
await this.delete(secondaryId, ctx);
|
|
2455
|
+
await this.events.publish(createEvent(CRM_EVENTS.CONTACT_MERGED, {
|
|
2456
|
+
primaryId,
|
|
2457
|
+
secondaryId,
|
|
2458
|
+
strategy
|
|
2459
|
+
}, ctx));
|
|
2460
|
+
return updated;
|
|
2461
|
+
}
|
|
1861
2462
|
};
|
|
2463
|
+
function stripEmpty(obj) {
|
|
2464
|
+
const out = {};
|
|
2465
|
+
for (const [k, v] of Object.entries(obj)) if (v !== void 0 && v !== null && v !== "") out[k] = v;
|
|
2466
|
+
return out;
|
|
2467
|
+
}
|
|
2468
|
+
/**
|
|
2469
|
+
* Convert a Mongoose subdocument (or plain object) into a plain JS object.
|
|
2470
|
+
* Spreading a Mongoose subdoc directly leaks `$__parent` / `_doc` etc.
|
|
2471
|
+
*/
|
|
2472
|
+
function plainOf(value) {
|
|
2473
|
+
if (value === null || value === void 0) return {};
|
|
2474
|
+
if (typeof value.toObject === "function") return value.toObject();
|
|
2475
|
+
return value;
|
|
2476
|
+
}
|
|
1862
2477
|
//#endregion
|
|
1863
2478
|
//#region src/repositories/account.repository.ts
|
|
1864
2479
|
var AccountRepository = class extends Repository {
|
|
@@ -1875,7 +2490,72 @@ var AccountRepository = class extends Repository {
|
|
|
1875
2490
|
}, ctx));
|
|
1876
2491
|
return created;
|
|
1877
2492
|
}
|
|
2493
|
+
async enrich(id, patch, extras = {}, ctx) {
|
|
2494
|
+
if (!await this.getById(id, ctx)) throw new NotFoundError("Account", id);
|
|
2495
|
+
const updated = await this.update(id, patch, ctx);
|
|
2496
|
+
if (!updated) throw new NotFoundError("Account", id);
|
|
2497
|
+
await this.events.publish(createEvent(CRM_EVENTS.ACCOUNT_ENRICHED, {
|
|
2498
|
+
accountId: id,
|
|
2499
|
+
fields: Object.keys(patch),
|
|
2500
|
+
...extras.provider !== void 0 ? { provider: extras.provider } : {}
|
|
2501
|
+
}, ctx));
|
|
2502
|
+
return updated;
|
|
2503
|
+
}
|
|
2504
|
+
async assign(id, ownerId, ctx) {
|
|
2505
|
+
const before = await this.findOneAndUpdate({
|
|
2506
|
+
_id: id,
|
|
2507
|
+
ownerId: { $ne: ownerId }
|
|
2508
|
+
}, { $set: { ownerId } }, {
|
|
2509
|
+
...ctx,
|
|
2510
|
+
returnDocument: "before"
|
|
2511
|
+
});
|
|
2512
|
+
if (!before) {
|
|
2513
|
+
const current = await this.getById(id, ctx);
|
|
2514
|
+
if (!current) throw new NotFoundError("Account", id);
|
|
2515
|
+
return current;
|
|
2516
|
+
}
|
|
2517
|
+
await this.events.publish(createEvent(CRM_EVENTS.ACCOUNT_ASSIGNED, {
|
|
2518
|
+
accountId: id,
|
|
2519
|
+
ownerId,
|
|
2520
|
+
...before.ownerId !== void 0 ? { previousOwnerId: before.ownerId } : {}
|
|
2521
|
+
}, ctx));
|
|
2522
|
+
return await this.getById(id, ctx) ?? before;
|
|
2523
|
+
}
|
|
2524
|
+
async merge(input, ctx) {
|
|
2525
|
+
const { primaryId, secondaryId } = input;
|
|
2526
|
+
if (primaryId === secondaryId) throw new Error("[crm] cannot merge an account with itself");
|
|
2527
|
+
const strategy = input.strategy ?? "preferPrimary";
|
|
2528
|
+
const [primary, secondary] = await Promise.all([this.getById(primaryId, ctx), this.getById(secondaryId, ctx)]);
|
|
2529
|
+
if (!primary) throw new NotFoundError("Account", primaryId);
|
|
2530
|
+
if (!secondary) throw new NotFoundError("Account", secondaryId);
|
|
2531
|
+
const winner = strategy === "preferPrimary" ? primary : secondary;
|
|
2532
|
+
const loser = strategy === "preferPrimary" ? secondary : primary;
|
|
2533
|
+
const mergedTags = Array.from(new Set([...primary.tags ?? [], ...secondary.tags ?? []]));
|
|
2534
|
+
const patch = {
|
|
2535
|
+
name: winner.name || loser.name,
|
|
2536
|
+
tags: mergedTags,
|
|
2537
|
+
...firstDefined("domain", winner.domain, loser.domain),
|
|
2538
|
+
...firstDefined("industry", winner.industry, loser.industry),
|
|
2539
|
+
...firstDefined("sizeBucket", winner.sizeBucket, loser.sizeBucket),
|
|
2540
|
+
...firstDefined("annualRevenueAmount", winner.annualRevenueAmount, loser.annualRevenueAmount),
|
|
2541
|
+
...firstDefined("ownerId", winner.ownerId, loser.ownerId)
|
|
2542
|
+
};
|
|
2543
|
+
const updated = await this.update(primaryId, patch, ctx);
|
|
2544
|
+
if (!updated) throw new NotFoundError("Account", primaryId);
|
|
2545
|
+
await this.delete(secondaryId, ctx);
|
|
2546
|
+
await this.events.publish(createEvent(CRM_EVENTS.ACCOUNT_MERGED, {
|
|
2547
|
+
primaryId,
|
|
2548
|
+
secondaryId,
|
|
2549
|
+
strategy
|
|
2550
|
+
}, ctx));
|
|
2551
|
+
return updated;
|
|
2552
|
+
}
|
|
1878
2553
|
};
|
|
2554
|
+
function firstDefined(key, a, b) {
|
|
2555
|
+
if (a !== void 0 && a !== null) return { [key]: a };
|
|
2556
|
+
if (b !== void 0 && b !== null) return { [key]: b };
|
|
2557
|
+
return {};
|
|
2558
|
+
}
|
|
1879
2559
|
//#endregion
|
|
1880
2560
|
//#region src/repositories/campaign.repository.ts
|
|
1881
2561
|
var CampaignRepository = class extends Repository {
|
|
@@ -1900,7 +2580,13 @@ var CampaignRepository = class extends Repository {
|
|
|
1900
2580
|
occurredAt: now
|
|
1901
2581
|
}]
|
|
1902
2582
|
};
|
|
1903
|
-
|
|
2583
|
+
let created;
|
|
2584
|
+
try {
|
|
2585
|
+
created = await this.create(draft, ctx);
|
|
2586
|
+
} catch (err) {
|
|
2587
|
+
if (this.rejectDuplicates && input.name && isDuplicateKeyError(err)) throw new DuplicateCampaignError(input.name);
|
|
2588
|
+
throw err;
|
|
2589
|
+
}
|
|
1904
2590
|
await this.events.publish(createEvent(CRM_EVENTS.CAMPAIGN_CREATED, {
|
|
1905
2591
|
campaignId: String(created._id),
|
|
1906
2592
|
name: created.name
|
|
@@ -2020,7 +2706,16 @@ var SubscriptionRepository = class extends Repository {
|
|
|
2020
2706
|
...input.consentUserAgent !== void 0 ? { consentUserAgent: input.consentUserAgent } : {},
|
|
2021
2707
|
...input.metadata !== void 0 ? { metadata: input.metadata } : {}
|
|
2022
2708
|
};
|
|
2023
|
-
|
|
2709
|
+
let sub;
|
|
2710
|
+
try {
|
|
2711
|
+
sub = await this.create(draft, ctx);
|
|
2712
|
+
} catch (err) {
|
|
2713
|
+
if (isDuplicateKeyError(err)) throw new DuplicateSubscriptionError({
|
|
2714
|
+
contactId: input.contactId,
|
|
2715
|
+
campaignId: input.campaignId
|
|
2716
|
+
});
|
|
2717
|
+
throw err;
|
|
2718
|
+
}
|
|
2024
2719
|
await this.events.publish(createEvent(CRM_EVENTS.SUBSCRIPTION_CREATED, {
|
|
2025
2720
|
subscriptionId: String(sub._id),
|
|
2026
2721
|
contactId: sub.contactId,
|
|
@@ -2164,6 +2859,55 @@ var ActivityRepository = class extends Repository {
|
|
|
2164
2859
|
}, ctx));
|
|
2165
2860
|
return updated;
|
|
2166
2861
|
}
|
|
2862
|
+
/**
|
|
2863
|
+
* Push an activity's `dueDate`. Allowed on planned / in_progress only;
|
|
2864
|
+
* rescheduling a completed or cancelled activity makes no sense.
|
|
2865
|
+
* Emits `crm:activity.rescheduled` with the previous date for audit.
|
|
2866
|
+
*/
|
|
2867
|
+
async reschedule(id, newDueDate, extras = {}, ctx) {
|
|
2868
|
+
const existing = await this.getById(id, ctx);
|
|
2869
|
+
if (!existing) throw new NotFoundError("Activity", id);
|
|
2870
|
+
if (existing.status !== "planned" && existing.status !== "in_progress") throw new InvalidActivityTransitionError({
|
|
2871
|
+
from: existing.status,
|
|
2872
|
+
to: existing.status,
|
|
2873
|
+
activityId: id
|
|
2874
|
+
});
|
|
2875
|
+
const previous = existing.dueDate;
|
|
2876
|
+
const updated = await this.update(id, { dueDate: newDueDate }, ctx);
|
|
2877
|
+
if (!updated) throw new NotFoundError("Activity", id);
|
|
2878
|
+
await this.events.publish(createEvent(CRM_EVENTS.ACTIVITY_RESCHEDULED, {
|
|
2879
|
+
activityId: id,
|
|
2880
|
+
dueDate: newDueDate.toISOString(),
|
|
2881
|
+
...previous ? { previousDueDate: previous.toISOString() } : {},
|
|
2882
|
+
...extras.reason !== void 0 ? { reason: extras.reason } : {},
|
|
2883
|
+
...extras.by !== void 0 ? { by: extras.by } : {}
|
|
2884
|
+
}, ctx));
|
|
2885
|
+
return updated;
|
|
2886
|
+
}
|
|
2887
|
+
/**
|
|
2888
|
+
* Record the outcome of an activity (call result, email engagement,
|
|
2889
|
+
* meeting confirmation). Stored under `metadata.outcome` so it stays
|
|
2890
|
+
* polymorphic — the shape is the channel's concern, not this package's.
|
|
2891
|
+
*
|
|
2892
|
+
* Does NOT transition status — call `complete()` separately when the
|
|
2893
|
+
* outcome implies completion. This split lets agents log partial
|
|
2894
|
+
* outcomes (e.g. "left voicemail") without forcing terminal state.
|
|
2895
|
+
*/
|
|
2896
|
+
async recordOutcome(id, outcome, ctx) {
|
|
2897
|
+
const existing = await this.getById(id, ctx);
|
|
2898
|
+
if (!existing) throw new NotFoundError("Activity", id);
|
|
2899
|
+
const mergedMetadata = {
|
|
2900
|
+
...existing.metadata ?? {},
|
|
2901
|
+
outcome
|
|
2902
|
+
};
|
|
2903
|
+
const updated = await this.update(id, { metadata: mergedMetadata }, ctx);
|
|
2904
|
+
if (!updated) throw new NotFoundError("Activity", id);
|
|
2905
|
+
await this.events.publish(createEvent(CRM_EVENTS.ACTIVITY_OUTCOME_RECORDED, {
|
|
2906
|
+
activityId: id,
|
|
2907
|
+
outcome
|
|
2908
|
+
}, ctx));
|
|
2909
|
+
return updated;
|
|
2910
|
+
}
|
|
2167
2911
|
async _transition(id, target, patch, ctx) {
|
|
2168
2912
|
const existing = await this.getById(id, ctx);
|
|
2169
2913
|
if (!existing) throw new NotFoundError("Activity", id);
|
|
@@ -2181,6 +2925,29 @@ var ActivityRepository = class extends Repository {
|
|
|
2181
2925
|
});
|
|
2182
2926
|
return updated;
|
|
2183
2927
|
}
|
|
2928
|
+
/** Count `planned` activities with `dueDate` before `asOf`. Dashboard KPI. */
|
|
2929
|
+
async countOverduePlanned(asOf, ctx) {
|
|
2930
|
+
return this.count({
|
|
2931
|
+
status: "planned",
|
|
2932
|
+
dueDate: { $lt: asOf }
|
|
2933
|
+
}, ctx);
|
|
2934
|
+
}
|
|
2935
|
+
/** Latest `limit` activities — lightweight projection for the activity feed. */
|
|
2936
|
+
async findRecentForFeed(limit, ctx) {
|
|
2937
|
+
return this.findAll({}, {
|
|
2938
|
+
...ctx,
|
|
2939
|
+
sort: { updatedAt: -1 },
|
|
2940
|
+
limit,
|
|
2941
|
+
select: {
|
|
2942
|
+
_id: 1,
|
|
2943
|
+
title: 1,
|
|
2944
|
+
status: 1,
|
|
2945
|
+
updatedAt: 1,
|
|
2946
|
+
subjectKind: 1,
|
|
2947
|
+
subjectId: 1
|
|
2948
|
+
}
|
|
2949
|
+
});
|
|
2950
|
+
}
|
|
2184
2951
|
};
|
|
2185
2952
|
//#endregion
|
|
2186
2953
|
//#region src/repositories/note.repository.ts
|
|
@@ -2269,6 +3036,161 @@ var EmailEventRepository = class extends Repository {
|
|
|
2269
3036
|
}
|
|
2270
3037
|
};
|
|
2271
3038
|
//#endregion
|
|
3039
|
+
//#region src/repositories/conversation.repository.ts
|
|
3040
|
+
var ConversationRepository = class extends Repository {
|
|
3041
|
+
events;
|
|
3042
|
+
constructor(model, plugins, deps, pagination) {
|
|
3043
|
+
super(model, plugins, pagination);
|
|
3044
|
+
this.events = deps.events;
|
|
3045
|
+
}
|
|
3046
|
+
async openConversation(input, ctx) {
|
|
3047
|
+
const draft = {
|
|
3048
|
+
channel: input.channel,
|
|
3049
|
+
participantKind: input.participantKind,
|
|
3050
|
+
participantId: input.participantId,
|
|
3051
|
+
status: "open",
|
|
3052
|
+
unreadCount: 0,
|
|
3053
|
+
...input.channelUserId !== void 0 ? { channelUserId: input.channelUserId } : {},
|
|
3054
|
+
...input.assigneeId !== void 0 ? { assigneeId: input.assigneeId } : {},
|
|
3055
|
+
...input.metadata !== void 0 ? { metadata: input.metadata } : {}
|
|
3056
|
+
};
|
|
3057
|
+
const created = await this.create(draft, ctx);
|
|
3058
|
+
await this.events.publish(createEvent(CRM_EVENTS.CONVERSATION_CREATED, {
|
|
3059
|
+
conversationId: String(created._id),
|
|
3060
|
+
channel: created.channel,
|
|
3061
|
+
participantKind: created.participantKind,
|
|
3062
|
+
participantId: String(created.participantId)
|
|
3063
|
+
}, ctx));
|
|
3064
|
+
return created;
|
|
3065
|
+
}
|
|
3066
|
+
async assign(id, assigneeId, ctx) {
|
|
3067
|
+
const before = await this.findOneAndUpdate({
|
|
3068
|
+
_id: id,
|
|
3069
|
+
assigneeId: { $ne: assigneeId }
|
|
3070
|
+
}, { $set: { assigneeId } }, {
|
|
3071
|
+
...ctx,
|
|
3072
|
+
returnDocument: "before"
|
|
3073
|
+
});
|
|
3074
|
+
if (!before) {
|
|
3075
|
+
const current = await this.getById(id, ctx);
|
|
3076
|
+
if (!current) throw new NotFoundError("Conversation", id);
|
|
3077
|
+
return current;
|
|
3078
|
+
}
|
|
3079
|
+
await this.events.publish(createEvent(CRM_EVENTS.CONVERSATION_ASSIGNED, {
|
|
3080
|
+
conversationId: id,
|
|
3081
|
+
assigneeId,
|
|
3082
|
+
...before.assigneeId !== void 0 ? { previousAssigneeId: before.assigneeId } : {}
|
|
3083
|
+
}, ctx));
|
|
3084
|
+
return await this.getById(id, ctx) ?? before;
|
|
3085
|
+
}
|
|
3086
|
+
async resolve(id, ctx) {
|
|
3087
|
+
return this._setStatus(id, "resolved", CRM_EVENTS.CONVERSATION_RESOLVED, ctx);
|
|
3088
|
+
}
|
|
3089
|
+
async reopen(id, ctx) {
|
|
3090
|
+
return this._setStatus(id, "open", CRM_EVENTS.CONVERSATION_REOPENED, ctx);
|
|
3091
|
+
}
|
|
3092
|
+
/** Reset unread badge. Pure UI affordance, no event. */
|
|
3093
|
+
async markRead(id, ctx) {
|
|
3094
|
+
const updated = await this.update(id, { unreadCount: 0 }, ctx);
|
|
3095
|
+
if (!updated) throw new NotFoundError("Conversation", id);
|
|
3096
|
+
return updated;
|
|
3097
|
+
}
|
|
3098
|
+
/**
|
|
3099
|
+
* Bump `lastMessageAt` and (for inbound) increment `unreadCount`.
|
|
3100
|
+
* Called by `MessageRepository` when a message is recorded — not part
|
|
3101
|
+
* of the public surface. Returns the conversation for the caller's
|
|
3102
|
+
* follow-up writes.
|
|
3103
|
+
*/
|
|
3104
|
+
async touch(id, args, ctx) {
|
|
3105
|
+
const existing = await this.getById(id, ctx);
|
|
3106
|
+
if (!existing) return null;
|
|
3107
|
+
const patch = { lastMessageAt: args.sentAt };
|
|
3108
|
+
if (args.direction === "inbound") patch.unreadCount = (existing.unreadCount ?? 0) + 1;
|
|
3109
|
+
return await this.update(id, patch, ctx) ?? null;
|
|
3110
|
+
}
|
|
3111
|
+
async _setStatus(id, next, eventType, ctx) {
|
|
3112
|
+
const existing = await this.getById(id, ctx);
|
|
3113
|
+
if (!existing) throw new NotFoundError("Conversation", id);
|
|
3114
|
+
if (existing.status === next) return existing;
|
|
3115
|
+
const updated = await this.update(id, { status: next }, ctx);
|
|
3116
|
+
if (!updated) throw new NotFoundError("Conversation", id);
|
|
3117
|
+
await this.events.publish(createEvent(eventType, {
|
|
3118
|
+
conversationId: id,
|
|
3119
|
+
status: next,
|
|
3120
|
+
previousStatus: existing.status
|
|
3121
|
+
}, ctx));
|
|
3122
|
+
return updated;
|
|
3123
|
+
}
|
|
3124
|
+
};
|
|
3125
|
+
//#endregion
|
|
3126
|
+
//#region src/repositories/message.repository.ts
|
|
3127
|
+
var MessageRepository = class extends Repository {
|
|
3128
|
+
events;
|
|
3129
|
+
conversations;
|
|
3130
|
+
constructor(model, plugins, deps, pagination) {
|
|
3131
|
+
super(model, plugins, pagination);
|
|
3132
|
+
this.events = deps.events;
|
|
3133
|
+
this.conversations = deps.conversations;
|
|
3134
|
+
}
|
|
3135
|
+
async recordInbound(input, ctx) {
|
|
3136
|
+
return this._record(input, "inbound", "delivered", CRM_EVENTS.MESSAGE_RECEIVED, ctx);
|
|
3137
|
+
}
|
|
3138
|
+
async recordOutbound(input, ctx) {
|
|
3139
|
+
return this._record(input, "outbound", "sent", CRM_EVENTS.MESSAGE_SENT, ctx);
|
|
3140
|
+
}
|
|
3141
|
+
/** Wire delivery callback (e.g. WhatsApp `status` webhook). */
|
|
3142
|
+
async updateStatus(id, status, ctx) {
|
|
3143
|
+
const existing = await this.getById(id, ctx);
|
|
3144
|
+
if (!existing) throw new NotFoundError("Message", id);
|
|
3145
|
+
if (existing.status === status) return existing;
|
|
3146
|
+
const updated = await this.update(id, { status }, ctx);
|
|
3147
|
+
if (!updated) throw new NotFoundError("Message", id);
|
|
3148
|
+
return updated;
|
|
3149
|
+
}
|
|
3150
|
+
async _record(input, direction, defaultStatus, eventType, ctx) {
|
|
3151
|
+
const sentAt = input.sentAt ?? /* @__PURE__ */ new Date();
|
|
3152
|
+
const draft = {
|
|
3153
|
+
conversationId: input.conversationId,
|
|
3154
|
+
direction,
|
|
3155
|
+
body: input.body,
|
|
3156
|
+
attachments: input.attachments ?? [],
|
|
3157
|
+
status: defaultStatus,
|
|
3158
|
+
sentAt,
|
|
3159
|
+
...input.externalMessageId !== void 0 ? { externalMessageId: input.externalMessageId } : {},
|
|
3160
|
+
...input.senderRef !== void 0 ? { senderRef: input.senderRef } : {},
|
|
3161
|
+
...input.recipientRef !== void 0 ? { recipientRef: input.recipientRef } : {},
|
|
3162
|
+
...input.metadata !== void 0 ? { metadata: input.metadata } : {}
|
|
3163
|
+
};
|
|
3164
|
+
const created = await this.create(draft, ctx);
|
|
3165
|
+
await this.conversations.touch(input.conversationId, {
|
|
3166
|
+
direction,
|
|
3167
|
+
sentAt
|
|
3168
|
+
}, ctx);
|
|
3169
|
+
await this.events.publish(createEvent(eventType, {
|
|
3170
|
+
messageId: String(created._id),
|
|
3171
|
+
conversationId: input.conversationId,
|
|
3172
|
+
direction,
|
|
3173
|
+
...input.externalMessageId !== void 0 ? { externalMessageId: input.externalMessageId } : {}
|
|
3174
|
+
}, ctx));
|
|
3175
|
+
return created;
|
|
3176
|
+
}
|
|
3177
|
+
/** Latest `limit` messages — lightweight projection for the activity feed. */
|
|
3178
|
+
async findRecentForFeed(limit, ctx) {
|
|
3179
|
+
return this.findAll({}, {
|
|
3180
|
+
...ctx,
|
|
3181
|
+
sort: { sentAt: -1 },
|
|
3182
|
+
limit,
|
|
3183
|
+
select: {
|
|
3184
|
+
_id: 1,
|
|
3185
|
+
body: 1,
|
|
3186
|
+
direction: 1,
|
|
3187
|
+
conversationId: 1,
|
|
3188
|
+
sentAt: 1
|
|
3189
|
+
}
|
|
3190
|
+
});
|
|
3191
|
+
}
|
|
3192
|
+
};
|
|
3193
|
+
//#endregion
|
|
2272
3194
|
//#region src/repositories/create-repositories.ts
|
|
2273
3195
|
/**
|
|
2274
3196
|
* Repository factory — composes the shared mongokit plugin stack and
|
|
@@ -2306,6 +3228,11 @@ function createCrmRepositories(models, deps) {
|
|
|
2306
3228
|
const note = new NoteRepository(models.Note, plugins, { events }, pagination);
|
|
2307
3229
|
const lossReason = new LossReasonRepository(models.LossReason, plugins, { events }, pagination);
|
|
2308
3230
|
const emailEvent = new EmailEventRepository(models.EmailEvent, plugins, { events }, pagination);
|
|
3231
|
+
const conversation = new ConversationRepository(models.Conversation, plugins, { events }, pagination);
|
|
3232
|
+
const message = new MessageRepository(models.Message, plugins, {
|
|
3233
|
+
events,
|
|
3234
|
+
conversations: conversation
|
|
3235
|
+
}, pagination);
|
|
2309
3236
|
return Object.freeze({
|
|
2310
3237
|
pipeline,
|
|
2311
3238
|
lead,
|
|
@@ -2317,7 +3244,9 @@ function createCrmRepositories(models, deps) {
|
|
|
2317
3244
|
activity,
|
|
2318
3245
|
note,
|
|
2319
3246
|
lossReason,
|
|
2320
|
-
emailEvent
|
|
3247
|
+
emailEvent,
|
|
3248
|
+
conversation,
|
|
3249
|
+
message
|
|
2321
3250
|
});
|
|
2322
3251
|
}
|
|
2323
3252
|
//#endregion
|
|
@@ -2358,6 +3287,24 @@ var InProcessCrmBus = class {
|
|
|
2358
3287
|
}
|
|
2359
3288
|
};
|
|
2360
3289
|
//#endregion
|
|
3290
|
+
//#region src/engine/ensure-ready.ts
|
|
3291
|
+
const REQUIRED_CAPABILITIES = [
|
|
3292
|
+
"upsert",
|
|
3293
|
+
"getOrCreate",
|
|
3294
|
+
"duplicateKeyError"
|
|
3295
|
+
];
|
|
3296
|
+
function assertCrmCapabilities(probe, options = {}) {
|
|
3297
|
+
const caps = probe.capabilities;
|
|
3298
|
+
if (!caps) throw new Error("[crm] backend declares no `capabilities` descriptor (requires @classytic/repo-core 0.6 / mongokit 3.16+).");
|
|
3299
|
+
const missing = REQUIRED_CAPABILITIES.filter((flag) => caps[flag] !== true);
|
|
3300
|
+
if (missing.length > 0) throw new Error(`[crm] backend missing required capabilities: ${missing.join(", ")}. Lead dedup + conversion depend on them.`);
|
|
3301
|
+
if (caps.transactions !== true) {
|
|
3302
|
+
const msg = "[crm] backend does not support transactions — LeadService.convert() cannot run atomically across Account/Contact/Opportunity. Run MongoDB as a replica set, or pass { allowNonTransactional: true } if convert() is never called with a session.";
|
|
3303
|
+
if (options.allowNonTransactional) options.logger?.warn?.(msg);
|
|
3304
|
+
else throw new Error(msg);
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
//#endregion
|
|
2361
3308
|
//#region src/engine/create-crm.ts
|
|
2362
3309
|
async function createCrm(config) {
|
|
2363
3310
|
if (!config.connection) throw new Error("[crm] CrmConfig.connection is required");
|
|
@@ -2365,6 +3312,8 @@ async function createCrm(config) {
|
|
|
2365
3312
|
connection: config.connection,
|
|
2366
3313
|
tenantFieldType: config.tenantFieldType ?? "objectId",
|
|
2367
3314
|
multiTenant: config.multiTenant ?? true,
|
|
3315
|
+
forceRecreate: config.forceRecreate ?? false,
|
|
3316
|
+
autoIndex: config.autoIndex ?? true,
|
|
2368
3317
|
...config.logger !== void 0 ? { logger: config.logger } : {},
|
|
2369
3318
|
...config.pagination !== void 0 ? { pagination: config.pagination } : {},
|
|
2370
3319
|
plugins: config.plugins ?? []
|
|
@@ -2375,11 +3324,18 @@ async function createCrm(config) {
|
|
|
2375
3324
|
events,
|
|
2376
3325
|
config: resolved
|
|
2377
3326
|
});
|
|
3327
|
+
assertCrmCapabilities(repositories.lead, {
|
|
3328
|
+
...config.allowNonTransactional !== void 0 ? { allowNonTransactional: config.allowNonTransactional } : {},
|
|
3329
|
+
...config.logger?.warn !== void 0 ? { logger: { warn: (m) => config.logger?.warn?.(m) } } : {}
|
|
3330
|
+
});
|
|
2378
3331
|
return Object.freeze({
|
|
2379
3332
|
models,
|
|
2380
3333
|
repositories,
|
|
2381
3334
|
events,
|
|
2382
3335
|
config: Object.freeze(resolved),
|
|
3336
|
+
async syncIndexes() {
|
|
3337
|
+
await Promise.all(Object.values(models).map((m) => m.syncIndexes()));
|
|
3338
|
+
},
|
|
2383
3339
|
async dispose() {
|
|
2384
3340
|
await events.close?.();
|
|
2385
3341
|
}
|
|
@@ -2561,6 +3517,602 @@ function splitLeadName(lead) {
|
|
|
2561
3517
|
};
|
|
2562
3518
|
}
|
|
2563
3519
|
//#endregion
|
|
3520
|
+
//#region src/events/event-catalog.ts
|
|
3521
|
+
/**
|
|
3522
|
+
* Zod-source event catalog (PACKAGE_RULES rule 18).
|
|
3523
|
+
*
|
|
3524
|
+
* One Zod schema per event is the source of truth — runtime validation,
|
|
3525
|
+
* static types, and Arc introspection all derive from it. Hosts wire the
|
|
3526
|
+
* definitions into arc's `EventRegistry`:
|
|
3527
|
+
*
|
|
3528
|
+
* ```ts
|
|
3529
|
+
* for (const def of crmEventDefinitions) registry.register(def);
|
|
3530
|
+
* ```
|
|
3531
|
+
*
|
|
3532
|
+
* which lights up publish-time validation (`eventPlugin({ registry,
|
|
3533
|
+
* validateMode: 'reject' })`), OpenAPI event introspection, and
|
|
3534
|
+
* `arc describe` output — with zero arc imports here (the definition shape
|
|
3535
|
+
* is structurally identical to arc's `EventDefinitionOutput`).
|
|
3536
|
+
*
|
|
3537
|
+
* Timestamps in payloads are `z.iso.datetime()` (NEVER `z.coerce.date()`):
|
|
3538
|
+
* `z.toJSONSchema()` throws on coerce.date, and events are ISO strings on
|
|
3539
|
+
* the wire (rule 25).
|
|
3540
|
+
*/
|
|
3541
|
+
function defineCrmEvent(input) {
|
|
3542
|
+
const { name, resource, description, version = 1, zodSchema } = input;
|
|
3543
|
+
return {
|
|
3544
|
+
name,
|
|
3545
|
+
version,
|
|
3546
|
+
description,
|
|
3547
|
+
zodSchema,
|
|
3548
|
+
schema: z.toJSONSchema(zodSchema),
|
|
3549
|
+
create(payload, meta) {
|
|
3550
|
+
return createEvent$1(name, payload, {
|
|
3551
|
+
resource,
|
|
3552
|
+
source: "crm",
|
|
3553
|
+
...meta
|
|
3554
|
+
});
|
|
3555
|
+
}
|
|
3556
|
+
};
|
|
3557
|
+
}
|
|
3558
|
+
const ownerChangeSchema = z.object({
|
|
3559
|
+
ownerId: z.string(),
|
|
3560
|
+
previousOwnerId: z.string().optional()
|
|
3561
|
+
});
|
|
3562
|
+
const enrichSchema = z.object({
|
|
3563
|
+
fields: z.array(z.string()),
|
|
3564
|
+
provider: z.string().optional()
|
|
3565
|
+
});
|
|
3566
|
+
const mergeSchema = z.object({
|
|
3567
|
+
primaryId: z.string(),
|
|
3568
|
+
secondaryId: z.string(),
|
|
3569
|
+
strategy: z.enum(["preferPrimary", "preferSecondary"])
|
|
3570
|
+
});
|
|
3571
|
+
const stageRefSchema = z.object({
|
|
3572
|
+
stageId: z.string(),
|
|
3573
|
+
sequence: z.number()
|
|
3574
|
+
});
|
|
3575
|
+
const LeadCreated = defineCrmEvent({
|
|
3576
|
+
name: CRM_EVENTS.LEAD_CREATED,
|
|
3577
|
+
resource: "lead",
|
|
3578
|
+
description: "createLead() inserted a scored lead.",
|
|
3579
|
+
zodSchema: z.object({
|
|
3580
|
+
leadId: z.string(),
|
|
3581
|
+
email: z.string().optional(),
|
|
3582
|
+
score: z.number()
|
|
3583
|
+
})
|
|
3584
|
+
});
|
|
3585
|
+
const LeadStatusChanged = defineCrmEvent({
|
|
3586
|
+
name: CRM_EVENTS.LEAD_STATUS_CHANGED,
|
|
3587
|
+
resource: "lead",
|
|
3588
|
+
description: "A lead lifecycle transition committed (qualify/disqualify/nurture/...).",
|
|
3589
|
+
zodSchema: z.object({
|
|
3590
|
+
leadId: z.string(),
|
|
3591
|
+
from: z.string(),
|
|
3592
|
+
to: z.string()
|
|
3593
|
+
})
|
|
3594
|
+
});
|
|
3595
|
+
const LeadScored = defineCrmEvent({
|
|
3596
|
+
name: CRM_EVENTS.LEAD_SCORED,
|
|
3597
|
+
resource: "lead",
|
|
3598
|
+
description: "A lead score was (re)computed and persisted.",
|
|
3599
|
+
zodSchema: z.object({
|
|
3600
|
+
leadId: z.string(),
|
|
3601
|
+
score: z.number()
|
|
3602
|
+
})
|
|
3603
|
+
});
|
|
3604
|
+
const LeadConverted = defineCrmEvent({
|
|
3605
|
+
name: CRM_EVENTS.LEAD_CONVERTED,
|
|
3606
|
+
resource: "lead",
|
|
3607
|
+
description: "convert() spawned Contact (+ Account) + Opportunity and closed the lead.",
|
|
3608
|
+
zodSchema: z.object({
|
|
3609
|
+
leadId: z.string(),
|
|
3610
|
+
contactId: z.string(),
|
|
3611
|
+
accountId: z.string().optional(),
|
|
3612
|
+
opportunityId: z.string()
|
|
3613
|
+
})
|
|
3614
|
+
});
|
|
3615
|
+
const LeadDisqualified = defineCrmEvent({
|
|
3616
|
+
name: CRM_EVENTS.LEAD_DISQUALIFIED,
|
|
3617
|
+
resource: "lead",
|
|
3618
|
+
description: "disqualify() rejected a lead with a reason.",
|
|
3619
|
+
zodSchema: z.object({
|
|
3620
|
+
leadId: z.string(),
|
|
3621
|
+
reason: z.string()
|
|
3622
|
+
})
|
|
3623
|
+
});
|
|
3624
|
+
const LeadEnriched = defineCrmEvent({
|
|
3625
|
+
name: CRM_EVENTS.LEAD_ENRICHED,
|
|
3626
|
+
resource: "lead",
|
|
3627
|
+
description: "enrich() merged a provider/manual patch into the lead.",
|
|
3628
|
+
zodSchema: z.object({ leadId: z.string() }).extend(enrichSchema.shape)
|
|
3629
|
+
});
|
|
3630
|
+
const LeadAssigned = defineCrmEvent({
|
|
3631
|
+
name: CRM_EVENTS.LEAD_ASSIGNED,
|
|
3632
|
+
resource: "lead",
|
|
3633
|
+
description: "assign() (re)assigned the lead owner.",
|
|
3634
|
+
zodSchema: z.object({ leadId: z.string() }).extend(ownerChangeSchema.shape)
|
|
3635
|
+
});
|
|
3636
|
+
const AccountCreated = defineCrmEvent({
|
|
3637
|
+
name: CRM_EVENTS.ACCOUNT_CREATED,
|
|
3638
|
+
resource: "account",
|
|
3639
|
+
description: "createAccount() inserted an account.",
|
|
3640
|
+
zodSchema: z.object({
|
|
3641
|
+
accountId: z.string(),
|
|
3642
|
+
name: z.string()
|
|
3643
|
+
})
|
|
3644
|
+
});
|
|
3645
|
+
const AccountUpdated = defineCrmEvent({
|
|
3646
|
+
name: CRM_EVENTS.ACCOUNT_UPDATED,
|
|
3647
|
+
resource: "account",
|
|
3648
|
+
description: "An account record was updated.",
|
|
3649
|
+
zodSchema: z.object({ accountId: z.string() })
|
|
3650
|
+
});
|
|
3651
|
+
const AccountEnriched = defineCrmEvent({
|
|
3652
|
+
name: CRM_EVENTS.ACCOUNT_ENRICHED,
|
|
3653
|
+
resource: "account",
|
|
3654
|
+
description: "enrich() merged a patch into the account.",
|
|
3655
|
+
zodSchema: z.object({ accountId: z.string() }).extend(enrichSchema.shape)
|
|
3656
|
+
});
|
|
3657
|
+
const AccountAssigned = defineCrmEvent({
|
|
3658
|
+
name: CRM_EVENTS.ACCOUNT_ASSIGNED,
|
|
3659
|
+
resource: "account",
|
|
3660
|
+
description: "assign() (re)assigned the account owner.",
|
|
3661
|
+
zodSchema: z.object({ accountId: z.string() }).extend(ownerChangeSchema.shape)
|
|
3662
|
+
});
|
|
3663
|
+
const AccountMerged = defineCrmEvent({
|
|
3664
|
+
name: CRM_EVENTS.ACCOUNT_MERGED,
|
|
3665
|
+
resource: "account",
|
|
3666
|
+
description: "merge() folded a secondary account into the primary; secondary deleted.",
|
|
3667
|
+
zodSchema: mergeSchema
|
|
3668
|
+
});
|
|
3669
|
+
const ContactCreated = defineCrmEvent({
|
|
3670
|
+
name: CRM_EVENTS.CONTACT_CREATED,
|
|
3671
|
+
resource: "contact",
|
|
3672
|
+
description: "createContact() inserted a contact.",
|
|
3673
|
+
zodSchema: z.object({
|
|
3674
|
+
contactId: z.string(),
|
|
3675
|
+
email: z.string().optional()
|
|
3676
|
+
})
|
|
3677
|
+
});
|
|
3678
|
+
const ContactUpdated = defineCrmEvent({
|
|
3679
|
+
name: CRM_EVENTS.CONTACT_UPDATED,
|
|
3680
|
+
resource: "contact",
|
|
3681
|
+
description: "A contact record was updated.",
|
|
3682
|
+
zodSchema: z.object({ contactId: z.string() })
|
|
3683
|
+
});
|
|
3684
|
+
const ContactEnriched = defineCrmEvent({
|
|
3685
|
+
name: CRM_EVENTS.CONTACT_ENRICHED,
|
|
3686
|
+
resource: "contact",
|
|
3687
|
+
description: "enrich() merged a patch into the contact.",
|
|
3688
|
+
zodSchema: z.object({ contactId: z.string() }).extend(enrichSchema.shape)
|
|
3689
|
+
});
|
|
3690
|
+
const ContactAssigned = defineCrmEvent({
|
|
3691
|
+
name: CRM_EVENTS.CONTACT_ASSIGNED,
|
|
3692
|
+
resource: "contact",
|
|
3693
|
+
description: "assign() (re)assigned the contact owner.",
|
|
3694
|
+
zodSchema: z.object({ contactId: z.string() }).extend(ownerChangeSchema.shape)
|
|
3695
|
+
});
|
|
3696
|
+
const ContactMerged = defineCrmEvent({
|
|
3697
|
+
name: CRM_EVENTS.CONTACT_MERGED,
|
|
3698
|
+
resource: "contact",
|
|
3699
|
+
description: "merge() folded a secondary contact into the primary; secondary deleted.",
|
|
3700
|
+
zodSchema: mergeSchema
|
|
3701
|
+
});
|
|
3702
|
+
const OpportunityCreated = defineCrmEvent({
|
|
3703
|
+
name: CRM_EVENTS.OPPORTUNITY_CREATED,
|
|
3704
|
+
resource: "opportunity",
|
|
3705
|
+
description: "openOpportunity() created a deal in a pipeline stage.",
|
|
3706
|
+
zodSchema: z.object({
|
|
3707
|
+
opportunityId: z.string(),
|
|
3708
|
+
pipelineId: z.string(),
|
|
3709
|
+
stageId: z.string()
|
|
3710
|
+
})
|
|
3711
|
+
});
|
|
3712
|
+
const OpportunityStageAdvanced = defineCrmEvent({
|
|
3713
|
+
name: CRM_EVENTS.OPPORTUNITY_STAGE_ADVANCED,
|
|
3714
|
+
resource: "opportunity",
|
|
3715
|
+
description: "moveToStage() moved the deal forward (higher stage sequence).",
|
|
3716
|
+
zodSchema: z.object({
|
|
3717
|
+
opportunityId: z.string(),
|
|
3718
|
+
from: stageRefSchema,
|
|
3719
|
+
to: stageRefSchema
|
|
3720
|
+
})
|
|
3721
|
+
});
|
|
3722
|
+
const OpportunityStageRegressed = defineCrmEvent({
|
|
3723
|
+
name: CRM_EVENTS.OPPORTUNITY_STAGE_REGRESSED,
|
|
3724
|
+
resource: "opportunity",
|
|
3725
|
+
description: "moveToStage() moved the deal backward (lower stage sequence).",
|
|
3726
|
+
zodSchema: z.object({
|
|
3727
|
+
opportunityId: z.string(),
|
|
3728
|
+
from: stageRefSchema,
|
|
3729
|
+
to: stageRefSchema
|
|
3730
|
+
})
|
|
3731
|
+
});
|
|
3732
|
+
const OpportunityWon = defineCrmEvent({
|
|
3733
|
+
name: CRM_EVENTS.OPPORTUNITY_WON,
|
|
3734
|
+
resource: "opportunity",
|
|
3735
|
+
description: "win() closed the deal as won (terminal).",
|
|
3736
|
+
zodSchema: z.object({
|
|
3737
|
+
opportunityId: z.string(),
|
|
3738
|
+
amount: z.number().optional()
|
|
3739
|
+
})
|
|
3740
|
+
});
|
|
3741
|
+
const OpportunityLost = defineCrmEvent({
|
|
3742
|
+
name: CRM_EVENTS.OPPORTUNITY_LOST,
|
|
3743
|
+
resource: "opportunity",
|
|
3744
|
+
description: "lose() closed the deal as lost (terminal).",
|
|
3745
|
+
zodSchema: z.object({
|
|
3746
|
+
opportunityId: z.string(),
|
|
3747
|
+
lostReasonId: z.string().optional()
|
|
3748
|
+
})
|
|
3749
|
+
});
|
|
3750
|
+
const OpportunityAbandoned = defineCrmEvent({
|
|
3751
|
+
name: CRM_EVENTS.OPPORTUNITY_ABANDONED,
|
|
3752
|
+
resource: "opportunity",
|
|
3753
|
+
description: "abandon() closed the deal as abandoned (terminal).",
|
|
3754
|
+
zodSchema: z.object({ opportunityId: z.string() })
|
|
3755
|
+
});
|
|
3756
|
+
const OpportunityAssigned = defineCrmEvent({
|
|
3757
|
+
name: CRM_EVENTS.OPPORTUNITY_ASSIGNED,
|
|
3758
|
+
resource: "opportunity",
|
|
3759
|
+
description: "assign() (re)assigned the opportunity owner.",
|
|
3760
|
+
zodSchema: z.object({ opportunityId: z.string() }).extend(ownerChangeSchema.shape)
|
|
3761
|
+
});
|
|
3762
|
+
const OpportunityAmountChanged = defineCrmEvent({
|
|
3763
|
+
name: CRM_EVENTS.OPPORTUNITY_AMOUNT_CHANGED,
|
|
3764
|
+
resource: "opportunity",
|
|
3765
|
+
description: "updateAmount() changed an open deal value.",
|
|
3766
|
+
zodSchema: z.object({
|
|
3767
|
+
opportunityId: z.string(),
|
|
3768
|
+
amount: z.number(),
|
|
3769
|
+
previousAmount: z.number().optional(),
|
|
3770
|
+
reason: z.string().optional(),
|
|
3771
|
+
by: z.string().optional()
|
|
3772
|
+
})
|
|
3773
|
+
});
|
|
3774
|
+
const OpportunityExtended = defineCrmEvent({
|
|
3775
|
+
name: CRM_EVENTS.OPPORTUNITY_EXTENDED,
|
|
3776
|
+
resource: "opportunity",
|
|
3777
|
+
description: "extendCloseDate() pushed expectedCloseAt further out.",
|
|
3778
|
+
zodSchema: z.object({
|
|
3779
|
+
opportunityId: z.string(),
|
|
3780
|
+
expectedCloseAt: z.iso.datetime(),
|
|
3781
|
+
previousExpectedCloseAt: z.iso.datetime().optional(),
|
|
3782
|
+
reason: z.string().optional(),
|
|
3783
|
+
by: z.string().optional()
|
|
3784
|
+
})
|
|
3785
|
+
});
|
|
3786
|
+
const PipelineCreated = defineCrmEvent({
|
|
3787
|
+
name: CRM_EVENTS.PIPELINE_CREATED,
|
|
3788
|
+
resource: "pipeline",
|
|
3789
|
+
description: "A pipeline was created.",
|
|
3790
|
+
zodSchema: z.object({ pipelineId: z.string() })
|
|
3791
|
+
});
|
|
3792
|
+
const PipelineArchived = defineCrmEvent({
|
|
3793
|
+
name: CRM_EVENTS.PIPELINE_ARCHIVED,
|
|
3794
|
+
resource: "pipeline",
|
|
3795
|
+
description: "archive() soft-deprecated a pipeline.",
|
|
3796
|
+
zodSchema: z.object({ pipelineId: z.string() })
|
|
3797
|
+
});
|
|
3798
|
+
const ActivityPlanned = defineCrmEvent({
|
|
3799
|
+
name: CRM_EVENTS.ACTIVITY_PLANNED,
|
|
3800
|
+
resource: "activity",
|
|
3801
|
+
description: "plan() created a planned activity against a subject.",
|
|
3802
|
+
zodSchema: z.object({
|
|
3803
|
+
activityId: z.string(),
|
|
3804
|
+
type: z.string(),
|
|
3805
|
+
subjectKind: z.string(),
|
|
3806
|
+
subjectId: z.string()
|
|
3807
|
+
})
|
|
3808
|
+
});
|
|
3809
|
+
const ActivityCompleted = defineCrmEvent({
|
|
3810
|
+
name: CRM_EVENTS.ACTIVITY_COMPLETED,
|
|
3811
|
+
resource: "activity",
|
|
3812
|
+
description: "complete() finished an activity.",
|
|
3813
|
+
zodSchema: z.object({
|
|
3814
|
+
activityId: z.string(),
|
|
3815
|
+
completedAt: z.iso.datetime()
|
|
3816
|
+
})
|
|
3817
|
+
});
|
|
3818
|
+
const ActivityCancelled = defineCrmEvent({
|
|
3819
|
+
name: CRM_EVENTS.ACTIVITY_CANCELLED,
|
|
3820
|
+
resource: "activity",
|
|
3821
|
+
description: "cancel() cancelled an activity.",
|
|
3822
|
+
zodSchema: z.object({
|
|
3823
|
+
activityId: z.string(),
|
|
3824
|
+
reason: z.string().optional()
|
|
3825
|
+
})
|
|
3826
|
+
});
|
|
3827
|
+
const ActivityRescheduled = defineCrmEvent({
|
|
3828
|
+
name: CRM_EVENTS.ACTIVITY_RESCHEDULED,
|
|
3829
|
+
resource: "activity",
|
|
3830
|
+
description: "reschedule() moved an activity due date.",
|
|
3831
|
+
zodSchema: z.object({
|
|
3832
|
+
activityId: z.string(),
|
|
3833
|
+
dueDate: z.iso.datetime(),
|
|
3834
|
+
previousDueDate: z.iso.datetime().optional(),
|
|
3835
|
+
reason: z.string().optional(),
|
|
3836
|
+
by: z.string().optional()
|
|
3837
|
+
})
|
|
3838
|
+
});
|
|
3839
|
+
const ActivityOutcomeRecorded = defineCrmEvent({
|
|
3840
|
+
name: CRM_EVENTS.ACTIVITY_OUTCOME_RECORDED,
|
|
3841
|
+
resource: "activity",
|
|
3842
|
+
description: "recordOutcome() logged an activity outcome (no status change).",
|
|
3843
|
+
zodSchema: z.object({
|
|
3844
|
+
activityId: z.string(),
|
|
3845
|
+
outcome: z.record(z.string(), z.unknown())
|
|
3846
|
+
})
|
|
3847
|
+
});
|
|
3848
|
+
const NoteAdded = defineCrmEvent({
|
|
3849
|
+
name: CRM_EVENTS.NOTE_ADDED,
|
|
3850
|
+
resource: "note",
|
|
3851
|
+
description: "A note was appended to a subject timeline.",
|
|
3852
|
+
zodSchema: z.object({
|
|
3853
|
+
noteId: z.string(),
|
|
3854
|
+
subjectKind: z.string(),
|
|
3855
|
+
subjectId: z.string()
|
|
3856
|
+
})
|
|
3857
|
+
});
|
|
3858
|
+
const CampaignCreated = defineCrmEvent({
|
|
3859
|
+
name: CRM_EVENTS.CAMPAIGN_CREATED,
|
|
3860
|
+
resource: "campaign",
|
|
3861
|
+
description: "createCampaign() inserted a campaign.",
|
|
3862
|
+
zodSchema: z.object({
|
|
3863
|
+
campaignId: z.string(),
|
|
3864
|
+
name: z.string()
|
|
3865
|
+
})
|
|
3866
|
+
});
|
|
3867
|
+
const CampaignActivated = defineCrmEvent({
|
|
3868
|
+
name: CRM_EVENTS.CAMPAIGN_ACTIVATED,
|
|
3869
|
+
resource: "campaign",
|
|
3870
|
+
description: "activate() moved a campaign to active.",
|
|
3871
|
+
zodSchema: z.object({ campaignId: z.string() })
|
|
3872
|
+
});
|
|
3873
|
+
const CampaignPaused = defineCrmEvent({
|
|
3874
|
+
name: CRM_EVENTS.CAMPAIGN_PAUSED,
|
|
3875
|
+
resource: "campaign",
|
|
3876
|
+
description: "pause() suspended an active campaign.",
|
|
3877
|
+
zodSchema: z.object({
|
|
3878
|
+
campaignId: z.string(),
|
|
3879
|
+
reason: z.string().optional()
|
|
3880
|
+
})
|
|
3881
|
+
});
|
|
3882
|
+
const CampaignResumed = defineCrmEvent({
|
|
3883
|
+
name: CRM_EVENTS.CAMPAIGN_RESUMED,
|
|
3884
|
+
resource: "campaign",
|
|
3885
|
+
description: "resume() reactivated a paused campaign.",
|
|
3886
|
+
zodSchema: z.object({ campaignId: z.string() })
|
|
3887
|
+
});
|
|
3888
|
+
const CampaignCompleted = defineCrmEvent({
|
|
3889
|
+
name: CRM_EVENTS.CAMPAIGN_COMPLETED,
|
|
3890
|
+
resource: "campaign",
|
|
3891
|
+
description: "complete() closed a campaign.",
|
|
3892
|
+
zodSchema: z.object({
|
|
3893
|
+
campaignId: z.string(),
|
|
3894
|
+
actualRevenueAmount: z.number().optional()
|
|
3895
|
+
})
|
|
3896
|
+
});
|
|
3897
|
+
const CampaignArchived = defineCrmEvent({
|
|
3898
|
+
name: CRM_EVENTS.CAMPAIGN_ARCHIVED,
|
|
3899
|
+
resource: "campaign",
|
|
3900
|
+
description: "archive() archived a campaign (terminal).",
|
|
3901
|
+
zodSchema: z.object({ campaignId: z.string() })
|
|
3902
|
+
});
|
|
3903
|
+
const SubscriptionCreated = defineCrmEvent({
|
|
3904
|
+
name: CRM_EVENTS.SUBSCRIPTION_CREATED,
|
|
3905
|
+
resource: "subscription",
|
|
3906
|
+
description: "subscribe() created an opt-in (pending or confirmed).",
|
|
3907
|
+
zodSchema: z.object({
|
|
3908
|
+
subscriptionId: z.string(),
|
|
3909
|
+
contactId: z.string(),
|
|
3910
|
+
campaignId: z.string(),
|
|
3911
|
+
requiresConfirmation: z.boolean()
|
|
3912
|
+
})
|
|
3913
|
+
});
|
|
3914
|
+
const SubscriptionConfirmed = defineCrmEvent({
|
|
3915
|
+
name: CRM_EVENTS.SUBSCRIPTION_CONFIRMED,
|
|
3916
|
+
resource: "subscription",
|
|
3917
|
+
description: "confirm() validated a double-opt-in token.",
|
|
3918
|
+
zodSchema: z.object({
|
|
3919
|
+
subscriptionId: z.string(),
|
|
3920
|
+
contactId: z.string(),
|
|
3921
|
+
campaignId: z.string(),
|
|
3922
|
+
confirmedAt: z.iso.datetime()
|
|
3923
|
+
})
|
|
3924
|
+
});
|
|
3925
|
+
const SubscriptionUnsubscribed = defineCrmEvent({
|
|
3926
|
+
name: CRM_EVENTS.SUBSCRIPTION_UNSUBSCRIBED,
|
|
3927
|
+
resource: "subscription",
|
|
3928
|
+
description: "unsubscribe() opted a contact out (terminal).",
|
|
3929
|
+
zodSchema: z.object({
|
|
3930
|
+
subscriptionId: z.string(),
|
|
3931
|
+
contactId: z.string(),
|
|
3932
|
+
campaignId: z.string(),
|
|
3933
|
+
reason: z.string().optional()
|
|
3934
|
+
})
|
|
3935
|
+
});
|
|
3936
|
+
const SubscriptionBounced = defineCrmEvent({
|
|
3937
|
+
name: CRM_EVENTS.SUBSCRIPTION_BOUNCED,
|
|
3938
|
+
resource: "subscription",
|
|
3939
|
+
description: "markBounced() recorded a hard bounce (terminal).",
|
|
3940
|
+
zodSchema: z.object({
|
|
3941
|
+
subscriptionId: z.string(),
|
|
3942
|
+
contactId: z.string(),
|
|
3943
|
+
campaignId: z.string(),
|
|
3944
|
+
reason: z.string().optional()
|
|
3945
|
+
})
|
|
3946
|
+
});
|
|
3947
|
+
const SubscriptionComplained = defineCrmEvent({
|
|
3948
|
+
name: CRM_EVENTS.SUBSCRIPTION_COMPLAINED,
|
|
3949
|
+
resource: "subscription",
|
|
3950
|
+
description: "markComplained() recorded a spam complaint (terminal).",
|
|
3951
|
+
zodSchema: z.object({
|
|
3952
|
+
subscriptionId: z.string(),
|
|
3953
|
+
contactId: z.string(),
|
|
3954
|
+
campaignId: z.string()
|
|
3955
|
+
})
|
|
3956
|
+
});
|
|
3957
|
+
const emailEventSchema = z.object({
|
|
3958
|
+
eventId: z.string(),
|
|
3959
|
+
kind: z.string(),
|
|
3960
|
+
subjectKind: z.string(),
|
|
3961
|
+
subjectId: z.string(),
|
|
3962
|
+
campaignId: z.string().optional()
|
|
3963
|
+
});
|
|
3964
|
+
const EmailEventOpened = defineCrmEvent({
|
|
3965
|
+
name: CRM_EVENTS.EMAIL_EVENT_OPENED,
|
|
3966
|
+
resource: "email_event",
|
|
3967
|
+
description: "recordOpen() logged an email open.",
|
|
3968
|
+
zodSchema: emailEventSchema
|
|
3969
|
+
});
|
|
3970
|
+
const EmailEventClicked = defineCrmEvent({
|
|
3971
|
+
name: CRM_EVENTS.EMAIL_EVENT_CLICKED,
|
|
3972
|
+
resource: "email_event",
|
|
3973
|
+
description: "recordClick() logged a link click.",
|
|
3974
|
+
zodSchema: emailEventSchema
|
|
3975
|
+
});
|
|
3976
|
+
const EmailEventBounced = defineCrmEvent({
|
|
3977
|
+
name: CRM_EVENTS.EMAIL_EVENT_BOUNCED,
|
|
3978
|
+
resource: "email_event",
|
|
3979
|
+
description: "recordBounce() logged a bounce.",
|
|
3980
|
+
zodSchema: emailEventSchema
|
|
3981
|
+
});
|
|
3982
|
+
const EmailEventComplained = defineCrmEvent({
|
|
3983
|
+
name: CRM_EVENTS.EMAIL_EVENT_COMPLAINED,
|
|
3984
|
+
resource: "email_event",
|
|
3985
|
+
description: "recordComplaint() logged a spam complaint.",
|
|
3986
|
+
zodSchema: emailEventSchema
|
|
3987
|
+
});
|
|
3988
|
+
const EmailEventUnsubscribed = defineCrmEvent({
|
|
3989
|
+
name: CRM_EVENTS.EMAIL_EVENT_UNSUBSCRIBED,
|
|
3990
|
+
resource: "email_event",
|
|
3991
|
+
description: "recordUnsubscribe() logged a one-click unsubscribe.",
|
|
3992
|
+
zodSchema: emailEventSchema
|
|
3993
|
+
});
|
|
3994
|
+
const ConversationCreated = defineCrmEvent({
|
|
3995
|
+
name: CRM_EVENTS.CONVERSATION_CREATED,
|
|
3996
|
+
resource: "conversation",
|
|
3997
|
+
description: "openConversation() started a conversation thread.",
|
|
3998
|
+
zodSchema: z.object({
|
|
3999
|
+
conversationId: z.string(),
|
|
4000
|
+
channel: z.string(),
|
|
4001
|
+
participantKind: z.string(),
|
|
4002
|
+
participantId: z.string()
|
|
4003
|
+
})
|
|
4004
|
+
});
|
|
4005
|
+
const ConversationAssigned = defineCrmEvent({
|
|
4006
|
+
name: CRM_EVENTS.CONVERSATION_ASSIGNED,
|
|
4007
|
+
resource: "conversation",
|
|
4008
|
+
description: "assign() (re)assigned a conversation owner.",
|
|
4009
|
+
zodSchema: z.object({
|
|
4010
|
+
conversationId: z.string(),
|
|
4011
|
+
assigneeId: z.string(),
|
|
4012
|
+
previousAssigneeId: z.string().optional()
|
|
4013
|
+
})
|
|
4014
|
+
});
|
|
4015
|
+
const ConversationResolved = defineCrmEvent({
|
|
4016
|
+
name: CRM_EVENTS.CONVERSATION_RESOLVED,
|
|
4017
|
+
resource: "conversation",
|
|
4018
|
+
description: "resolve() closed a conversation.",
|
|
4019
|
+
zodSchema: z.object({
|
|
4020
|
+
conversationId: z.string(),
|
|
4021
|
+
status: z.string(),
|
|
4022
|
+
previousStatus: z.string()
|
|
4023
|
+
})
|
|
4024
|
+
});
|
|
4025
|
+
const ConversationReopened = defineCrmEvent({
|
|
4026
|
+
name: CRM_EVENTS.CONVERSATION_REOPENED,
|
|
4027
|
+
resource: "conversation",
|
|
4028
|
+
description: "reopen() reopened a resolved conversation.",
|
|
4029
|
+
zodSchema: z.object({
|
|
4030
|
+
conversationId: z.string(),
|
|
4031
|
+
status: z.string(),
|
|
4032
|
+
previousStatus: z.string()
|
|
4033
|
+
})
|
|
4034
|
+
});
|
|
4035
|
+
const MessageReceived = defineCrmEvent({
|
|
4036
|
+
name: CRM_EVENTS.MESSAGE_RECEIVED,
|
|
4037
|
+
resource: "message",
|
|
4038
|
+
description: "recordInbound() logged an inbound message.",
|
|
4039
|
+
zodSchema: z.object({
|
|
4040
|
+
messageId: z.string(),
|
|
4041
|
+
conversationId: z.string(),
|
|
4042
|
+
direction: z.string(),
|
|
4043
|
+
externalMessageId: z.string().optional()
|
|
4044
|
+
})
|
|
4045
|
+
});
|
|
4046
|
+
const MessageSent = defineCrmEvent({
|
|
4047
|
+
name: CRM_EVENTS.MESSAGE_SENT,
|
|
4048
|
+
resource: "message",
|
|
4049
|
+
description: "recordOutbound() logged an outbound message.",
|
|
4050
|
+
zodSchema: z.object({
|
|
4051
|
+
messageId: z.string(),
|
|
4052
|
+
conversationId: z.string(),
|
|
4053
|
+
direction: z.string(),
|
|
4054
|
+
externalMessageId: z.string().optional()
|
|
4055
|
+
})
|
|
4056
|
+
});
|
|
4057
|
+
const crmEventDefinitions = [
|
|
4058
|
+
LeadCreated,
|
|
4059
|
+
LeadStatusChanged,
|
|
4060
|
+
LeadScored,
|
|
4061
|
+
LeadConverted,
|
|
4062
|
+
LeadDisqualified,
|
|
4063
|
+
LeadEnriched,
|
|
4064
|
+
LeadAssigned,
|
|
4065
|
+
AccountCreated,
|
|
4066
|
+
AccountUpdated,
|
|
4067
|
+
AccountEnriched,
|
|
4068
|
+
AccountAssigned,
|
|
4069
|
+
AccountMerged,
|
|
4070
|
+
ContactCreated,
|
|
4071
|
+
ContactUpdated,
|
|
4072
|
+
ContactEnriched,
|
|
4073
|
+
ContactAssigned,
|
|
4074
|
+
ContactMerged,
|
|
4075
|
+
OpportunityCreated,
|
|
4076
|
+
OpportunityStageAdvanced,
|
|
4077
|
+
OpportunityStageRegressed,
|
|
4078
|
+
OpportunityWon,
|
|
4079
|
+
OpportunityLost,
|
|
4080
|
+
OpportunityAbandoned,
|
|
4081
|
+
OpportunityAssigned,
|
|
4082
|
+
OpportunityAmountChanged,
|
|
4083
|
+
OpportunityExtended,
|
|
4084
|
+
PipelineCreated,
|
|
4085
|
+
PipelineArchived,
|
|
4086
|
+
ActivityPlanned,
|
|
4087
|
+
ActivityCompleted,
|
|
4088
|
+
ActivityCancelled,
|
|
4089
|
+
ActivityRescheduled,
|
|
4090
|
+
ActivityOutcomeRecorded,
|
|
4091
|
+
NoteAdded,
|
|
4092
|
+
CampaignCreated,
|
|
4093
|
+
CampaignActivated,
|
|
4094
|
+
CampaignPaused,
|
|
4095
|
+
CampaignResumed,
|
|
4096
|
+
CampaignCompleted,
|
|
4097
|
+
CampaignArchived,
|
|
4098
|
+
SubscriptionCreated,
|
|
4099
|
+
SubscriptionConfirmed,
|
|
4100
|
+
SubscriptionUnsubscribed,
|
|
4101
|
+
SubscriptionBounced,
|
|
4102
|
+
SubscriptionComplained,
|
|
4103
|
+
EmailEventOpened,
|
|
4104
|
+
EmailEventClicked,
|
|
4105
|
+
EmailEventBounced,
|
|
4106
|
+
EmailEventComplained,
|
|
4107
|
+
EmailEventUnsubscribed,
|
|
4108
|
+
ConversationCreated,
|
|
4109
|
+
ConversationAssigned,
|
|
4110
|
+
ConversationResolved,
|
|
4111
|
+
ConversationReopened,
|
|
4112
|
+
MessageReceived,
|
|
4113
|
+
MessageSent
|
|
4114
|
+
];
|
|
4115
|
+
//#endregion
|
|
2564
4116
|
//#region src/shared/calculators/forecast.ts
|
|
2565
4117
|
/**
|
|
2566
4118
|
* Aggregate opportunities into a forecast table — one row per bucket.
|
|
@@ -2753,7 +4305,11 @@ const contactInfoSchema = z.object({
|
|
|
2753
4305
|
email: z.string().email().max(255).optional(),
|
|
2754
4306
|
personalEmail: z.string().email().max(255).optional(),
|
|
2755
4307
|
phone: z.string().max(50).optional(),
|
|
2756
|
-
alternatePhone: z.string().max(50).optional()
|
|
4308
|
+
alternatePhone: z.string().max(50).optional(),
|
|
4309
|
+
whatsapp: z.string().max(50).optional(),
|
|
4310
|
+
telegram: z.string().max(100).optional(),
|
|
4311
|
+
linkedin: z.string().max(255).optional(),
|
|
4312
|
+
instagram: z.string().max(100).optional()
|
|
2757
4313
|
});
|
|
2758
4314
|
const createContactSchema = z.object({
|
|
2759
4315
|
name: contactNameSchema,
|
|
@@ -2967,6 +4523,4 @@ var validators_exports = /* @__PURE__ */ __exportAll({
|
|
|
2967
4523
|
updatePipelineSchema: () => updatePipelineSchema
|
|
2968
4524
|
});
|
|
2969
4525
|
//#endregion
|
|
2970
|
-
export { ACTIVITY_MACHINE, ACTIVITY_STATUSES, ACTIVITY_TRANSITIONS, ACTIVITY_TYPES, AccountRepository, ActivityRepository, CAMPAIGN_MACHINE, CAMPAIGN_STATUSES, CAMPAIGN_TRANSITIONS, CRM_EVENTS, CampaignRepository, ContactRepository, CrmError, DuplicateCampaignError, DuplicateLeadError, DuplicateSubscriptionError, EMAIL_EVENT_KINDS, EMAIL_EVENT_SUBJECT_KINDS, EmailEventRepository, EmptyPipelineError, InProcessCrmBus, InvalidActivityTransitionError, InvalidCampaignTransitionError, InvalidConfirmationTokenError, InvalidLeadTransitionError, InvalidOpportunityTransitionError, InvalidSubscriptionTransitionError, InvalidUnsubscribeTokenError, LEAD_MACHINE, LEAD_STATUSES, LEAD_TRANSITIONS, LeadAlreadyConvertedError, LeadRepository, LeadService, LossReasonRepository, NotFoundError, NoteRepository, OPPORTUNITY_MACHINE, OPPORTUNITY_STATUSES, OPPORTUNITY_TRANSITIONS, OpportunityRepository, PipelineRepository, SUBJECT_KINDS, SUBSCRIPTION_MACHINE, SUBSCRIPTION_STATUSES, SUBSCRIPTION_TRANSITIONS, StageNotInPipelineError, SubscriptionRepository, appendStatus, buildAccountSchema, buildActivitySchema, buildCampaignSchema, buildContactSchema, buildEmailEventSchema, buildLeadSchema, buildLossReasonSchema, buildNoteSchema, buildOpportunitySchema, buildPipelineSchema, buildSubscriptionSchema, canTransitionActivity, canTransitionCampaign, canTransitionLead, canTransitionOpportunity, canTransitionSubscription, composeScorers, createAccountModel, createActivityModel, createCampaignModel, createContactModel, createCrm, createCrmModels, createCrmRepositories, createEmailEventModel, createEvent, createLeadModel, createLossReasonModel, createNoteModel, createOpportunityModel, createPipelineModel, createSubscriptionModel, defaultLeadScorer, forecast, lastTransitionTo, latestEntry, pipelineConversion, tenantFieldDef, timeInStatus, validators_exports as validators };
|
|
2971
|
-
|
|
2972
|
-
//# sourceMappingURL=index.js.map
|
|
4526
|
+
export { ACTIVITY_MACHINE, ACTIVITY_STATUSES, ACTIVITY_TRANSITIONS, ACTIVITY_TYPES, AccountAssigned, AccountCreated, AccountEnriched, AccountMerged, AccountRepository, AccountUpdated, ActivityCancelled, ActivityCompleted, ActivityOutcomeRecorded, ActivityPlanned, ActivityRepository, ActivityRescheduled, CAMPAIGN_MACHINE, CAMPAIGN_STATUSES, CAMPAIGN_TRANSITIONS, CONVERSATION_CHANNELS, CONVERSATION_PARTICIPANT_KINDS, CONVERSATION_STATUSES, CRM_EVENTS, CRM_MODEL_NAMES, CampaignActivated, CampaignArchived, CampaignCompleted, CampaignCreated, CampaignPaused, CampaignRepository, CampaignResumed, ContactAssigned, ContactCreated, ContactEnriched, ContactMerged, ContactRepository, ContactUpdated, ConversationAssigned, ConversationCreated, ConversationReopened, ConversationRepository, ConversationResolved, CrmError, DuplicateCampaignError, DuplicateLeadError, DuplicateSubscriptionError, EMAIL_EVENT_KINDS, EMAIL_EVENT_SUBJECT_KINDS, EmailEventBounced, EmailEventClicked, EmailEventComplained, EmailEventOpened, EmailEventRepository, EmailEventUnsubscribed, EmptyPipelineError, InProcessCrmBus, InvalidActivityTransitionError, InvalidCampaignTransitionError, InvalidConfirmationTokenError, InvalidLeadTransitionError, InvalidOpportunityTransitionError, InvalidSubscriptionTransitionError, InvalidUnsubscribeTokenError, LEAD_MACHINE, LEAD_STATUSES, LEAD_TRANSITIONS, LeadAlreadyConvertedError, LeadAssigned, LeadConverted, LeadCreated, LeadDisqualified, LeadEnriched, LeadRepository, LeadScored, LeadService, LeadStatusChanged, LossReasonRepository, MESSAGE_DELIVERY_STATUSES, MESSAGE_DIRECTIONS, MessageReceived, MessageRepository, MessageSent, NotFoundError, NoteAdded, NoteRepository, OPPORTUNITY_MACHINE, OPPORTUNITY_STATUSES, OPPORTUNITY_TRANSITIONS, OpportunityAbandoned, OpportunityAmountChanged, OpportunityAssigned, OpportunityCreated, OpportunityExtended, OpportunityLost, OpportunityRepository, OpportunityStageAdvanced, OpportunityStageRegressed, OpportunityWon, PipelineArchived, PipelineCreated, PipelineRepository, SUBJECT_KINDS, SUBSCRIPTION_MACHINE, SUBSCRIPTION_STATUSES, SUBSCRIPTION_TRANSITIONS, StageNotInPipelineError, SubscriptionBounced, SubscriptionComplained, SubscriptionConfirmed, SubscriptionCreated, SubscriptionRepository, SubscriptionUnsubscribed, appendStatus, buildAccountSchema, buildActivitySchema, buildCampaignSchema, buildContactSchema, buildConversationSchema, buildEmailEventSchema, buildLeadSchema, buildLossReasonSchema, buildMessageSchema, buildNoteSchema, buildOpportunitySchema, buildPipelineSchema, buildSubscriptionSchema, canTransitionActivity, canTransitionCampaign, canTransitionLead, canTransitionOpportunity, canTransitionSubscription, composeScorers, createAccountModel, createActivityModel, createCampaignModel, createContactModel, createConversationModel, createCrm, createCrmModels, createCrmRepositories, createEmailEventModel, createEvent, createLeadModel, createLossReasonModel, createMessageModel, createNoteModel, createOpportunityModel, createPipelineModel, createSubscriptionModel, crmEventDefinitions, defaultLeadScorer, forecast, lastTransitionTo, latestEntry, pipelineConversion, tenantFieldDef, timeInStatus, validators_exports as validators };
|