@classytic/crm 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,11 +1,104 @@
1
- import { t as __exportAll } from "./chunk.js";
1
+ import { t as __exportAll } from "./rolldown-runtime.js";
2
+ import { AccountAssigned, AccountCreated, AccountEnriched, AccountMerged, AccountUpdated, ActivityCancelled, ActivityCompleted, ActivityOutcomeRecorded, ActivityPlanned, ActivityRescheduled, CRM_EVENTS, CampaignActivated, CampaignArchived, CampaignCompleted, CampaignCreated, CampaignPaused, CampaignResumed, ContactAssigned, ContactCreated, ContactEnriched, ContactMerged, ContactUpdated, ConversationAssigned, ConversationCreated, ConversationReopened, ConversationResolved, EmailEventBounced, EmailEventClicked, EmailEventComplained, EmailEventOpened, EmailEventUnsubscribed, InProcessCrmBus, LeadAssigned, LeadConverted, LeadCreated, LeadDisqualified, LeadEnriched, LeadScored, LeadStatusChanged, MessageReceived, MessageSent, NoteAdded, OpportunityAbandoned, OpportunityAmountChanged, OpportunityAssigned, OpportunityCreated, OpportunityExtended, OpportunityLost, OpportunityStageAdvanced, OpportunityStageRegressed, OpportunityWon, PipelineArchived, PipelineCreated, SubscriptionBounced, SubscriptionComplained, SubscriptionConfirmed, SubscriptionCreated, SubscriptionUnsubscribed, createEvent, crmEventDefinitions } from "./events.js";
2
3
  import { Schema } from "mongoose";
3
- import { Repository, aggregateHelpersPlugin, methodRegistryPlugin, multiTenantPlugin } from "@classytic/mongokit";
4
- import { createEvent as createEvent$1, matchEventPattern } from "@classytic/primitives/events";
4
+ import { Repository, aggregateHelpersPlugin, isDuplicateKeyError, methodRegistryPlugin, multiTenantPlugin } from "@classytic/mongokit";
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
- * Subscription — a Contact's opt-in to a Campaign. Double-opt-in tokens,
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
- }, { timestamps: true });
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
- return connection.model("Subscription", buildSubscriptionSchema(config));
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,176 +1388,275 @@ function createEmailEventModel(connection, config) {
1255
1388
  return connection.model("EmailEvent", buildEmailEventSchema(config));
1256
1389
  }
1257
1390
  //#endregion
1258
- //#region src/models/create-models.ts
1259
- function createCrmModels(connection, config) {
1260
- return Object.freeze({
1261
- Pipeline: createPipelineModel(connection, config),
1262
- Lead: createLeadModel(connection, config),
1263
- Opportunity: createOpportunityModel(connection, config),
1264
- Contact: createContactModel(connection, config),
1265
- Account: createAccountModel(connection, config),
1266
- Campaign: createCampaignModel(connection, config),
1267
- Subscription: createSubscriptionModel(connection, config),
1268
- Activity: createActivityModel(connection, config),
1269
- Note: createNoteModel(connection, config),
1270
- LossReason: createLossReasonModel(connection, config),
1271
- EmailEvent: createEmailEventModel(connection, config)
1272
- });
1273
- }
1274
- //#endregion
1275
- //#region src/core/errors.ts
1391
+ //#region src/enums/conversation.ts
1276
1392
  /**
1277
- * CRM error vocabulary. Hosts catch `CrmError` at API edges.
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.
1278
1399
  */
1279
- var CrmError = class extends Error {
1280
- code;
1281
- details;
1282
- constructor(code, message, details) {
1283
- super(message);
1284
- this.name = "CrmError";
1285
- this.code = code;
1286
- if (details !== void 0) this.details = details;
1287
- }
1288
- };
1289
- var NotFoundError = class extends CrmError {
1290
- constructor(kind, id) {
1291
- super(`${kind.toUpperCase()}_NOT_FOUND`, `${kind} '${id}' not found`, {
1292
- kind,
1293
- id
1294
- });
1295
- }
1296
- };
1297
- var InvalidLeadTransitionError = class extends CrmError {
1298
- constructor(details) {
1299
- super("INVALID_LEAD_TRANSITION", `Lead ${details.leadId} cannot move from '${details.from}' to '${details.to}'`, details);
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);
1335
- }
1336
- };
1337
- var DuplicateCampaignError = class extends CrmError {
1338
- constructor(name) {
1339
- super("DUPLICATE_CAMPAIGN", `A campaign named '${name}' already exists`, { name });
1340
- }
1341
- };
1342
- var InvalidSubscriptionTransitionError = class extends CrmError {
1343
- constructor(details) {
1344
- super("INVALID_SUBSCRIPTION_TRANSITION", `Subscription ${details.subscriptionId} cannot move from '${details.from}' to '${details.to}'`, details);
1345
- }
1346
- };
1347
- var DuplicateSubscriptionError = class extends CrmError {
1348
- constructor(details) {
1349
- super("DUPLICATE_SUBSCRIPTION", `Contact '${details.contactId}' already has a live subscription on campaign '${details.campaignId}'`, details);
1350
- }
1351
- };
1352
- var InvalidConfirmationTokenError = class extends CrmError {
1353
- constructor() {
1354
- super("INVALID_CONFIRMATION_TOKEN", "No subscription matches the supplied confirmation token, or it has already been used");
1355
- }
1356
- };
1357
- var InvalidUnsubscribeTokenError = class extends CrmError {
1358
- constructor() {
1359
- super("INVALID_UNSUBSCRIBE_TOKEN", "No subscription matches the supplied unsubscribe token");
1360
- }
1361
- };
1362
- //#endregion
1363
- //#region src/events/event-constants.ts
1364
- /** PACKAGE_RULES rule 12: `package:resource.verb`. */
1365
- const CRM_EVENTS = {
1366
- LEAD_CREATED: "crm:lead.created",
1367
- LEAD_STATUS_CHANGED: "crm:lead.status_changed",
1368
- LEAD_SCORED: "crm:lead.scored",
1369
- LEAD_CONVERTED: "crm:lead.converted",
1370
- LEAD_DISQUALIFIED: "crm:lead.disqualified",
1371
- ACCOUNT_CREATED: "crm:account.created",
1372
- ACCOUNT_UPDATED: "crm:account.updated",
1373
- CONTACT_CREATED: "crm:contact.created",
1374
- CONTACT_UPDATED: "crm:contact.updated",
1375
- OPPORTUNITY_CREATED: "crm:opportunity.created",
1376
- OPPORTUNITY_STAGE_ADVANCED: "crm:opportunity.stage_advanced",
1377
- OPPORTUNITY_STAGE_REGRESSED: "crm:opportunity.stage_regressed",
1378
- OPPORTUNITY_WON: "crm:opportunity.won",
1379
- OPPORTUNITY_LOST: "crm:opportunity.lost",
1380
- OPPORTUNITY_ABANDONED: "crm:opportunity.abandoned",
1381
- PIPELINE_CREATED: "crm:pipeline.created",
1382
- PIPELINE_ARCHIVED: "crm:pipeline.archived",
1383
- ACTIVITY_PLANNED: "crm:activity.planned",
1384
- ACTIVITY_COMPLETED: "crm:activity.completed",
1385
- ACTIVITY_CANCELLED: "crm:activity.cancelled",
1386
- NOTE_ADDED: "crm:note.added",
1387
- CAMPAIGN_CREATED: "crm:campaign.created",
1388
- CAMPAIGN_ACTIVATED: "crm:campaign.activated",
1389
- CAMPAIGN_PAUSED: "crm:campaign.paused",
1390
- CAMPAIGN_RESUMED: "crm:campaign.resumed",
1391
- CAMPAIGN_COMPLETED: "crm:campaign.completed",
1392
- CAMPAIGN_ARCHIVED: "crm:campaign.archived",
1393
- SUBSCRIPTION_CREATED: "crm:subscription.created",
1394
- SUBSCRIPTION_CONFIRMED: "crm:subscription.confirmed",
1395
- SUBSCRIPTION_UNSUBSCRIBED: "crm:subscription.unsubscribed",
1396
- SUBSCRIPTION_BOUNCED: "crm:subscription.bounced",
1397
- SUBSCRIPTION_COMPLAINED: "crm:subscription.complained",
1398
- EMAIL_EVENT_OPENED: "crm:email_event.opened",
1399
- EMAIL_EVENT_CLICKED: "crm:email_event.clicked",
1400
- EMAIL_EVENT_BOUNCED: "crm:email_event.bounced",
1401
- EMAIL_EVENT_COMPLAINED: "crm:email_event.complained",
1402
- EMAIL_EVENT_UNSUBSCRIBED: "crm:email_event.unsubscribed"
1403
- };
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
+ ];
1404
1423
  //#endregion
1405
- //#region src/events/helpers.ts
1424
+ //#region src/models/conversation.schema.ts
1406
1425
  /**
1407
- * Event creation helper.
1426
+ * Conversation — a multi-channel message thread bound to a Contact or
1427
+ * (pre-conversion) Lead. One row per (channel, participant) pair.
1408
1428
  *
1409
- * Thin wrapper around `@classytic/primitives`' `createEvent` that fills the
1410
- * shared `EventMeta` from a `CrmContext` (userId/actorId, organizationId,
1411
- * correlationId). Use this instead of importing `createEvent` directly so
1412
- * the `source: 'crm'` stamp + tenant scoping stay consistent.
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).
1413
1433
  */
1414
- function createEvent(type, payload, ctx, meta) {
1415
- return createEvent$1(type, payload, {
1416
- source: "crm",
1417
- ...ctx?.actorId !== void 0 ? { actorId: String(ctx.actorId) } : {},
1418
- ...ctx?.organizationId !== void 0 ? { organizationId: String(ctx.organizationId) } : {},
1419
- ...ctx?.correlationId !== void 0 ? { correlationId: ctx.correlationId } : {},
1420
- ...meta
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
1421
1483
  });
1422
- }
1423
- //#endregion
1424
- //#region src/repositories/pipeline.repository.ts
1425
- var PipelineRepository = class extends Repository {
1426
- events;
1427
- constructor(model, plugins, deps, pagination) {
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
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));
1504
+ }
1505
+ //#endregion
1506
+ //#region src/models/message.schema.ts
1507
+ /**
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.
1516
+ */
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
1539
+ }
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
+ }
1655
+ //#endregion
1656
+ //#region src/repositories/pipeline.repository.ts
1657
+ var PipelineRepository = class extends Repository {
1658
+ events;
1659
+ constructor(model, plugins, deps, pagination) {
1428
1660
  super(model, plugins, pagination);
1429
1661
  this.events = deps.events;
1430
1662
  }
@@ -1582,7 +1814,6 @@ var LeadRepository = class extends Repository {
1582
1814
  const computedFullName = input.fullName ?? ([input.firstName, input.lastName].filter(Boolean).join(" ").trim() || input.email || input.phone);
1583
1815
  const draft = {
1584
1816
  ...input,
1585
- fullName: computedFullName,
1586
1817
  status: input.status ?? "new",
1587
1818
  score: 0,
1588
1819
  statusHistory: [{
@@ -1590,7 +1821,14 @@ var LeadRepository = class extends Repository {
1590
1821
  occurredAt: now
1591
1822
  }]
1592
1823
  };
1593
- const created = await this.create(draft, ctx);
1824
+ if (computedFullName !== void 0) draft.fullName = computedFullName;
1825
+ let created;
1826
+ try {
1827
+ created = await this.create(draft, ctx);
1828
+ } catch (err) {
1829
+ if (this.rejectDuplicates && input.email && isDuplicateKeyError(err)) throw new DuplicateLeadError({ email: input.email });
1830
+ throw err;
1831
+ }
1594
1832
  const score = this.scorer(created);
1595
1833
  let result = created;
1596
1834
  if (score !== 0) {
@@ -1629,6 +1867,50 @@ var LeadRepository = class extends Repository {
1629
1867
  return this._transition(id, "nurturing", extras, ctx);
1630
1868
  }
1631
1869
  /**
1870
+ * Merge enrichment payload (3rd-party provider response or manual
1871
+ * patch) into the lead doc, then re-score. Emits `crm:lead.enriched`
1872
+ * with the field set written so subscribers can fan out (e.g. push a
1873
+ * `recompute_segment` job) without re-reading the doc.
1874
+ *
1875
+ * The `provider` tag flows into the event for attribution; the package
1876
+ * is provider-agnostic so the actual enrichment call lives at the host.
1877
+ */
1878
+ async enrich(id, patch, extras = {}, ctx) {
1879
+ if (!await this.getById(id, ctx)) throw new NotFoundError("Lead", id);
1880
+ if (!await this.update(id, patch, ctx)) throw new NotFoundError("Lead", id);
1881
+ const fields = Object.keys(patch);
1882
+ await this.events.publish(createEvent(CRM_EVENTS.LEAD_ENRICHED, {
1883
+ leadId: id,
1884
+ fields,
1885
+ ...extras.provider !== void 0 ? { provider: extras.provider } : {}
1886
+ }, ctx));
1887
+ return this.recomputeScore(id, ctx);
1888
+ }
1889
+ /**
1890
+ * Assign / re-assign the lead's owner. Emits `crm:lead.assigned` with
1891
+ * the previous owner so audit consumers can compute hand-off chains.
1892
+ */
1893
+ async assign(id, ownerId, ctx) {
1894
+ const before = await this.findOneAndUpdate({
1895
+ _id: id,
1896
+ ownerId: { $ne: ownerId }
1897
+ }, { $set: { ownerId } }, {
1898
+ ...ctx,
1899
+ returnDocument: "before"
1900
+ });
1901
+ if (!before) {
1902
+ const current = await this.getById(id, ctx);
1903
+ if (!current) throw new NotFoundError("Lead", id);
1904
+ return current;
1905
+ }
1906
+ await this.events.publish(createEvent(CRM_EVENTS.LEAD_ASSIGNED, {
1907
+ leadId: id,
1908
+ ownerId,
1909
+ ...before.ownerId !== void 0 ? { previousOwnerId: before.ownerId } : {}
1910
+ }, ctx));
1911
+ return await this.getById(id, ctx) ?? before;
1912
+ }
1913
+ /**
1632
1914
  * Recompute + persist the lead's score. Called from `createLead`
1633
1915
  * automatically; hosts also call this after enrichment writes.
1634
1916
  */
@@ -1676,6 +1958,29 @@ var LeadRepository = class extends Repository {
1676
1958
  }, ctx));
1677
1959
  return updated;
1678
1960
  }
1961
+ /**
1962
+ * Count leads created on or after `since`. Used by dashboard KPI tiles.
1963
+ * Goes through the inherited tenant-scoped `count()` — the plugin
1964
+ * injects `organizationId` (never a manual `if (ctx)` filter, which
1965
+ * silently reads ALL tenants when ctx is absent).
1966
+ */
1967
+ async countCreatedSince(since, ctx) {
1968
+ return this.count({ createdAt: { $gte: since } }, ctx);
1969
+ }
1970
+ /** Latest `limit` leads — lightweight projection for the activity feed. */
1971
+ async findRecentForFeed(limit, ctx) {
1972
+ return this.findAll({}, {
1973
+ ...ctx,
1974
+ sort: { updatedAt: -1 },
1975
+ limit,
1976
+ select: {
1977
+ _id: 1,
1978
+ fullName: 1,
1979
+ status: 1,
1980
+ updatedAt: 1
1981
+ }
1982
+ });
1983
+ }
1679
1984
  };
1680
1985
  //#endregion
1681
1986
  //#region src/repositories/opportunity.repository.ts
@@ -1799,6 +2104,83 @@ var OpportunityRepository = class extends Repository {
1799
2104
  }, ctx));
1800
2105
  return updated;
1801
2106
  }
2107
+ /**
2108
+ * Re-assign opportunity owner. Mutates `ownerId` only; no status
2109
+ * transition. Emits `crm:opportunity.assigned` with the previous owner.
2110
+ */
2111
+ async assign(id, ownerId, ctx) {
2112
+ const before = await this.findOneAndUpdate({
2113
+ _id: id,
2114
+ ownerId: { $ne: ownerId }
2115
+ }, { $set: { ownerId } }, {
2116
+ ...ctx,
2117
+ returnDocument: "before"
2118
+ });
2119
+ if (!before) {
2120
+ const current = await this.getById(id, ctx);
2121
+ if (!current) throw new NotFoundError("Opportunity", id);
2122
+ return current;
2123
+ }
2124
+ await this.events.publish(createEvent(CRM_EVENTS.OPPORTUNITY_ASSIGNED, {
2125
+ opportunityId: id,
2126
+ ownerId,
2127
+ ...before.ownerId !== void 0 ? { previousOwnerId: before.ownerId } : {}
2128
+ }, ctx));
2129
+ return await this.getById(id, ctx) ?? before;
2130
+ }
2131
+ /**
2132
+ * Adjust the deal amount on an open opportunity. Rejected after
2133
+ * win/lose/abandon — closed-deal amounts are immutable history.
2134
+ * Emits `crm:opportunity.amount_changed` with the delta so forecast
2135
+ * subscribers can re-bucket.
2136
+ */
2137
+ async updateAmount(id, amount, extras = {}, ctx) {
2138
+ const existing = await this.getById(id, ctx);
2139
+ if (!existing) throw new NotFoundError("Opportunity", id);
2140
+ if (existing.status !== "open") throw new InvalidOpportunityTransitionError({
2141
+ from: existing.status,
2142
+ to: existing.status,
2143
+ opportunityId: id
2144
+ });
2145
+ if (amount === existing.amount) return existing;
2146
+ const updated = await this.update(id, { amount }, ctx);
2147
+ if (!updated) throw new NotFoundError("Opportunity", id);
2148
+ await this.events.publish(createEvent(CRM_EVENTS.OPPORTUNITY_AMOUNT_CHANGED, {
2149
+ opportunityId: id,
2150
+ amount,
2151
+ ...existing.amount !== void 0 ? { previousAmount: existing.amount } : {},
2152
+ ...extras.reason !== void 0 ? { reason: extras.reason } : {},
2153
+ ...extras.by !== void 0 ? { by: extras.by } : {}
2154
+ }, ctx));
2155
+ return updated;
2156
+ }
2157
+ /**
2158
+ * Push `expectedCloseAt` further out. Only valid on open opps —
2159
+ * extending a closed deal is meaningless. The new date must be later
2160
+ * than the current one; pulling the date in is silently accepted
2161
+ * (no event is meaningful there).
2162
+ */
2163
+ async extendCloseDate(id, newDate, extras = {}, ctx) {
2164
+ const existing = await this.getById(id, ctx);
2165
+ if (!existing) throw new NotFoundError("Opportunity", id);
2166
+ if (existing.status !== "open") throw new InvalidOpportunityTransitionError({
2167
+ from: existing.status,
2168
+ to: existing.status,
2169
+ opportunityId: id
2170
+ });
2171
+ const previous = existing.expectedCloseAt;
2172
+ if (previous && newDate <= previous) return existing;
2173
+ const updated = await this.update(id, { expectedCloseAt: newDate }, ctx);
2174
+ if (!updated) throw new NotFoundError("Opportunity", id);
2175
+ await this.events.publish(createEvent(CRM_EVENTS.OPPORTUNITY_EXTENDED, {
2176
+ opportunityId: id,
2177
+ expectedCloseAt: newDate.toISOString(),
2178
+ ...previous ? { previousExpectedCloseAt: previous.toISOString() } : {},
2179
+ ...extras.reason !== void 0 ? { reason: extras.reason } : {},
2180
+ ...extras.by !== void 0 ? { by: extras.by } : {}
2181
+ }, ctx));
2182
+ return updated;
2183
+ }
1802
2184
  async abandon(id, by, note, ctx) {
1803
2185
  const updated = await this._closeStatus(id, "abandoned", {
1804
2186
  closedAt: /* @__PURE__ */ new Date(),
@@ -1837,6 +2219,58 @@ var OpportunityRepository = class extends Repository {
1837
2219
  });
1838
2220
  return updated;
1839
2221
  }
2222
+ /** Count opportunities with status `open`. Used by dashboard KPI tiles. */
2223
+ async countOpen(ctx) {
2224
+ return this.count({ status: "open" }, ctx);
2225
+ }
2226
+ /**
2227
+ * Sum `amount` on won opportunities updated within `since`.
2228
+ * Goes through the tenant-scoped `findAll()` — the multiTenant plugin
2229
+ * injects `organizationId`, so a missing ctx can't leak other tenants'
2230
+ * revenue (a manual `if (ctx)` filter would).
2231
+ */
2232
+ async sumWonSince(since, ctx) {
2233
+ const rows = await this.findAll({
2234
+ status: "won",
2235
+ updatedAt: { $gte: since }
2236
+ }, {
2237
+ ...ctx,
2238
+ select: {
2239
+ amount: 1,
2240
+ currency: 1
2241
+ }
2242
+ });
2243
+ if (rows.length === 0) return null;
2244
+ const byCurrency = /* @__PURE__ */ new Map();
2245
+ for (const r of rows) {
2246
+ const cur = r.currency ?? "UNKNOWN";
2247
+ byCurrency.set(cur, (byCurrency.get(cur) ?? 0) + (r.amount ?? 0));
2248
+ }
2249
+ let topCurrency = "";
2250
+ let topTotal = -Infinity;
2251
+ for (const [cur, total] of byCurrency) if (total > topTotal) {
2252
+ topTotal = total;
2253
+ topCurrency = cur;
2254
+ }
2255
+ return {
2256
+ total: topTotal,
2257
+ currency: topCurrency
2258
+ };
2259
+ }
2260
+ /** Latest `limit` opportunities — lightweight projection for the activity feed. */
2261
+ async findRecentForFeed(limit, ctx) {
2262
+ return this.findAll({}, {
2263
+ ...ctx,
2264
+ sort: { updatedAt: -1 },
2265
+ limit,
2266
+ select: {
2267
+ _id: 1,
2268
+ name: 1,
2269
+ status: 1,
2270
+ updatedAt: 1
2271
+ }
2272
+ });
2273
+ }
1840
2274
  };
1841
2275
  //#endregion
1842
2276
  //#region src/repositories/contact.repository.ts
@@ -1858,7 +2292,108 @@ var ContactRepository = class extends Repository {
1858
2292
  }, ctx));
1859
2293
  return created;
1860
2294
  }
2295
+ /**
2296
+ * Merge enrichment payload into the contact. The `patch` is applied
2297
+ * verbatim; deep-merge of `contact.*` channels is the caller's job
2298
+ * (they own the provider response shape).
2299
+ */
2300
+ async enrich(id, patch, extras = {}, ctx) {
2301
+ if (!await this.getById(id, ctx)) throw new NotFoundError("Contact", id);
2302
+ const updated = await this.update(id, patch, ctx);
2303
+ if (!updated) throw new NotFoundError("Contact", id);
2304
+ await this.events.publish(createEvent(CRM_EVENTS.CONTACT_ENRICHED, {
2305
+ contactId: id,
2306
+ fields: Object.keys(patch),
2307
+ ...extras.provider !== void 0 ? { provider: extras.provider } : {}
2308
+ }, ctx));
2309
+ return updated;
2310
+ }
2311
+ /** Set / re-assign the contact's owner. Atomic; no-op when unchanged. */
2312
+ async assign(id, ownerId, ctx) {
2313
+ const before = await this.findOneAndUpdate({
2314
+ _id: id,
2315
+ ownerId: { $ne: ownerId }
2316
+ }, { $set: { ownerId } }, {
2317
+ ...ctx,
2318
+ returnDocument: "before"
2319
+ });
2320
+ if (!before) {
2321
+ const current = await this.getById(id, ctx);
2322
+ if (!current) throw new NotFoundError("Contact", id);
2323
+ return current;
2324
+ }
2325
+ await this.events.publish(createEvent(CRM_EVENTS.CONTACT_ASSIGNED, {
2326
+ contactId: id,
2327
+ ownerId,
2328
+ ...before.ownerId !== void 0 ? { previousOwnerId: before.ownerId } : {}
2329
+ }, ctx));
2330
+ return await this.getById(id, ctx) ?? before;
2331
+ }
2332
+ /**
2333
+ * Merge two contacts. Secondary's non-conflicting fields are folded
2334
+ * into primary, tags are unioned, then secondary is deleted.
2335
+ *
2336
+ * Hosts wanting transactional safety can pass `ctx.session` —
2337
+ * mongokit threads it through `update` + `delete`.
2338
+ *
2339
+ * Re-pointing related Activities / Notes / Opportunities is the host's
2340
+ * job (they own those relationships) — subscribe to
2341
+ * `crm:contact.merged` and rewrite `subjectId` / `primaryContactId`
2342
+ * from there.
2343
+ */
2344
+ async merge(input, ctx) {
2345
+ const { primaryId, secondaryId } = input;
2346
+ if (primaryId === secondaryId) throw new Error("[crm] cannot merge a contact with itself");
2347
+ const strategy = input.strategy ?? "preferPrimary";
2348
+ const [primary, secondary] = await Promise.all([this.getById(primaryId, ctx), this.getById(secondaryId, ctx)]);
2349
+ if (!primary) throw new NotFoundError("Contact", primaryId);
2350
+ if (!secondary) throw new NotFoundError("Contact", secondaryId);
2351
+ const winner = strategy === "preferPrimary" ? primary : secondary;
2352
+ const loser = strategy === "preferPrimary" ? secondary : primary;
2353
+ const winnerContact = plainOf(winner.contact);
2354
+ const loserContact = plainOf(loser.contact);
2355
+ const winnerName = plainOf(winner.name);
2356
+ const loserName = plainOf(loser.name);
2357
+ const mergedContactInfo = {
2358
+ ...loserContact,
2359
+ ...stripEmpty(winnerContact)
2360
+ };
2361
+ const patch = {
2362
+ name: {
2363
+ ...loserName,
2364
+ ...stripEmpty(winnerName)
2365
+ },
2366
+ contact: mergedContactInfo,
2367
+ tags: Array.from(new Set([...primary.tags ?? [], ...secondary.tags ?? []])),
2368
+ ...winner.jobTitle !== void 0 ? { jobTitle: winner.jobTitle } : loser.jobTitle !== void 0 ? { jobTitle: loser.jobTitle } : {},
2369
+ ...winner.accountId !== void 0 ? { accountId: winner.accountId } : loser.accountId !== void 0 ? { accountId: loser.accountId } : {},
2370
+ ...winner.ownerId !== void 0 ? { ownerId: winner.ownerId } : loser.ownerId !== void 0 ? { ownerId: loser.ownerId } : {}
2371
+ };
2372
+ const updated = await this.update(primaryId, patch, ctx);
2373
+ if (!updated) throw new NotFoundError("Contact", primaryId);
2374
+ await this.delete(secondaryId, ctx);
2375
+ await this.events.publish(createEvent(CRM_EVENTS.CONTACT_MERGED, {
2376
+ primaryId,
2377
+ secondaryId,
2378
+ strategy
2379
+ }, ctx));
2380
+ return updated;
2381
+ }
1861
2382
  };
2383
+ function stripEmpty(obj) {
2384
+ const out = {};
2385
+ for (const [k, v] of Object.entries(obj)) if (v !== void 0 && v !== null && v !== "") out[k] = v;
2386
+ return out;
2387
+ }
2388
+ /**
2389
+ * Convert a Mongoose subdocument (or plain object) into a plain JS object.
2390
+ * Spreading a Mongoose subdoc directly leaks `$__parent` / `_doc` etc.
2391
+ */
2392
+ function plainOf(value) {
2393
+ if (value === null || value === void 0) return {};
2394
+ if (typeof value.toObject === "function") return value.toObject();
2395
+ return value;
2396
+ }
1862
2397
  //#endregion
1863
2398
  //#region src/repositories/account.repository.ts
1864
2399
  var AccountRepository = class extends Repository {
@@ -1875,7 +2410,72 @@ var AccountRepository = class extends Repository {
1875
2410
  }, ctx));
1876
2411
  return created;
1877
2412
  }
2413
+ async enrich(id, patch, extras = {}, ctx) {
2414
+ if (!await this.getById(id, ctx)) throw new NotFoundError("Account", id);
2415
+ const updated = await this.update(id, patch, ctx);
2416
+ if (!updated) throw new NotFoundError("Account", id);
2417
+ await this.events.publish(createEvent(CRM_EVENTS.ACCOUNT_ENRICHED, {
2418
+ accountId: id,
2419
+ fields: Object.keys(patch),
2420
+ ...extras.provider !== void 0 ? { provider: extras.provider } : {}
2421
+ }, ctx));
2422
+ return updated;
2423
+ }
2424
+ async assign(id, ownerId, ctx) {
2425
+ const before = await this.findOneAndUpdate({
2426
+ _id: id,
2427
+ ownerId: { $ne: ownerId }
2428
+ }, { $set: { ownerId } }, {
2429
+ ...ctx,
2430
+ returnDocument: "before"
2431
+ });
2432
+ if (!before) {
2433
+ const current = await this.getById(id, ctx);
2434
+ if (!current) throw new NotFoundError("Account", id);
2435
+ return current;
2436
+ }
2437
+ await this.events.publish(createEvent(CRM_EVENTS.ACCOUNT_ASSIGNED, {
2438
+ accountId: id,
2439
+ ownerId,
2440
+ ...before.ownerId !== void 0 ? { previousOwnerId: before.ownerId } : {}
2441
+ }, ctx));
2442
+ return await this.getById(id, ctx) ?? before;
2443
+ }
2444
+ async merge(input, ctx) {
2445
+ const { primaryId, secondaryId } = input;
2446
+ if (primaryId === secondaryId) throw new Error("[crm] cannot merge an account with itself");
2447
+ const strategy = input.strategy ?? "preferPrimary";
2448
+ const [primary, secondary] = await Promise.all([this.getById(primaryId, ctx), this.getById(secondaryId, ctx)]);
2449
+ if (!primary) throw new NotFoundError("Account", primaryId);
2450
+ if (!secondary) throw new NotFoundError("Account", secondaryId);
2451
+ const winner = strategy === "preferPrimary" ? primary : secondary;
2452
+ const loser = strategy === "preferPrimary" ? secondary : primary;
2453
+ const mergedTags = Array.from(new Set([...primary.tags ?? [], ...secondary.tags ?? []]));
2454
+ const patch = {
2455
+ name: winner.name || loser.name,
2456
+ tags: mergedTags,
2457
+ ...firstDefined("domain", winner.domain, loser.domain),
2458
+ ...firstDefined("industry", winner.industry, loser.industry),
2459
+ ...firstDefined("sizeBucket", winner.sizeBucket, loser.sizeBucket),
2460
+ ...firstDefined("annualRevenueAmount", winner.annualRevenueAmount, loser.annualRevenueAmount),
2461
+ ...firstDefined("ownerId", winner.ownerId, loser.ownerId)
2462
+ };
2463
+ const updated = await this.update(primaryId, patch, ctx);
2464
+ if (!updated) throw new NotFoundError("Account", primaryId);
2465
+ await this.delete(secondaryId, ctx);
2466
+ await this.events.publish(createEvent(CRM_EVENTS.ACCOUNT_MERGED, {
2467
+ primaryId,
2468
+ secondaryId,
2469
+ strategy
2470
+ }, ctx));
2471
+ return updated;
2472
+ }
1878
2473
  };
2474
+ function firstDefined(key, a, b) {
2475
+ if (a !== void 0 && a !== null) return { [key]: a };
2476
+ if (b !== void 0 && b !== null) return { [key]: b };
2477
+ return {};
2478
+ }
1879
2479
  //#endregion
1880
2480
  //#region src/repositories/campaign.repository.ts
1881
2481
  var CampaignRepository = class extends Repository {
@@ -1900,7 +2500,13 @@ var CampaignRepository = class extends Repository {
1900
2500
  occurredAt: now
1901
2501
  }]
1902
2502
  };
1903
- const created = await this.create(draft, ctx);
2503
+ let created;
2504
+ try {
2505
+ created = await this.create(draft, ctx);
2506
+ } catch (err) {
2507
+ if (this.rejectDuplicates && input.name && isDuplicateKeyError(err)) throw new DuplicateCampaignError(input.name);
2508
+ throw err;
2509
+ }
1904
2510
  await this.events.publish(createEvent(CRM_EVENTS.CAMPAIGN_CREATED, {
1905
2511
  campaignId: String(created._id),
1906
2512
  name: created.name
@@ -2020,7 +2626,16 @@ var SubscriptionRepository = class extends Repository {
2020
2626
  ...input.consentUserAgent !== void 0 ? { consentUserAgent: input.consentUserAgent } : {},
2021
2627
  ...input.metadata !== void 0 ? { metadata: input.metadata } : {}
2022
2628
  };
2023
- const sub = await this.create(draft, ctx);
2629
+ let sub;
2630
+ try {
2631
+ sub = await this.create(draft, ctx);
2632
+ } catch (err) {
2633
+ if (isDuplicateKeyError(err)) throw new DuplicateSubscriptionError({
2634
+ contactId: input.contactId,
2635
+ campaignId: input.campaignId
2636
+ });
2637
+ throw err;
2638
+ }
2024
2639
  await this.events.publish(createEvent(CRM_EVENTS.SUBSCRIPTION_CREATED, {
2025
2640
  subscriptionId: String(sub._id),
2026
2641
  contactId: sub.contactId,
@@ -2164,6 +2779,55 @@ var ActivityRepository = class extends Repository {
2164
2779
  }, ctx));
2165
2780
  return updated;
2166
2781
  }
2782
+ /**
2783
+ * Push an activity's `dueDate`. Allowed on planned / in_progress only;
2784
+ * rescheduling a completed or cancelled activity makes no sense.
2785
+ * Emits `crm:activity.rescheduled` with the previous date for audit.
2786
+ */
2787
+ async reschedule(id, newDueDate, extras = {}, ctx) {
2788
+ const existing = await this.getById(id, ctx);
2789
+ if (!existing) throw new NotFoundError("Activity", id);
2790
+ if (existing.status !== "planned" && existing.status !== "in_progress") throw new InvalidActivityTransitionError({
2791
+ from: existing.status,
2792
+ to: existing.status,
2793
+ activityId: id
2794
+ });
2795
+ const previous = existing.dueDate;
2796
+ const updated = await this.update(id, { dueDate: newDueDate }, ctx);
2797
+ if (!updated) throw new NotFoundError("Activity", id);
2798
+ await this.events.publish(createEvent(CRM_EVENTS.ACTIVITY_RESCHEDULED, {
2799
+ activityId: id,
2800
+ dueDate: newDueDate.toISOString(),
2801
+ ...previous ? { previousDueDate: previous.toISOString() } : {},
2802
+ ...extras.reason !== void 0 ? { reason: extras.reason } : {},
2803
+ ...extras.by !== void 0 ? { by: extras.by } : {}
2804
+ }, ctx));
2805
+ return updated;
2806
+ }
2807
+ /**
2808
+ * Record the outcome of an activity (call result, email engagement,
2809
+ * meeting confirmation). Stored under `metadata.outcome` so it stays
2810
+ * polymorphic — the shape is the channel's concern, not this package's.
2811
+ *
2812
+ * Does NOT transition status — call `complete()` separately when the
2813
+ * outcome implies completion. This split lets agents log partial
2814
+ * outcomes (e.g. "left voicemail") without forcing terminal state.
2815
+ */
2816
+ async recordOutcome(id, outcome, ctx) {
2817
+ const existing = await this.getById(id, ctx);
2818
+ if (!existing) throw new NotFoundError("Activity", id);
2819
+ const mergedMetadata = {
2820
+ ...existing.metadata ?? {},
2821
+ outcome
2822
+ };
2823
+ const updated = await this.update(id, { metadata: mergedMetadata }, ctx);
2824
+ if (!updated) throw new NotFoundError("Activity", id);
2825
+ await this.events.publish(createEvent(CRM_EVENTS.ACTIVITY_OUTCOME_RECORDED, {
2826
+ activityId: id,
2827
+ outcome
2828
+ }, ctx));
2829
+ return updated;
2830
+ }
2167
2831
  async _transition(id, target, patch, ctx) {
2168
2832
  const existing = await this.getById(id, ctx);
2169
2833
  if (!existing) throw new NotFoundError("Activity", id);
@@ -2181,6 +2845,29 @@ var ActivityRepository = class extends Repository {
2181
2845
  });
2182
2846
  return updated;
2183
2847
  }
2848
+ /** Count `planned` activities with `dueDate` before `asOf`. Dashboard KPI. */
2849
+ async countOverduePlanned(asOf, ctx) {
2850
+ return this.count({
2851
+ status: "planned",
2852
+ dueDate: { $lt: asOf }
2853
+ }, ctx);
2854
+ }
2855
+ /** Latest `limit` activities — lightweight projection for the activity feed. */
2856
+ async findRecentForFeed(limit, ctx) {
2857
+ return this.findAll({}, {
2858
+ ...ctx,
2859
+ sort: { updatedAt: -1 },
2860
+ limit,
2861
+ select: {
2862
+ _id: 1,
2863
+ title: 1,
2864
+ status: 1,
2865
+ updatedAt: 1,
2866
+ subjectKind: 1,
2867
+ subjectId: 1
2868
+ }
2869
+ });
2870
+ }
2184
2871
  };
2185
2872
  //#endregion
2186
2873
  //#region src/repositories/note.repository.ts
@@ -2269,6 +2956,161 @@ var EmailEventRepository = class extends Repository {
2269
2956
  }
2270
2957
  };
2271
2958
  //#endregion
2959
+ //#region src/repositories/conversation.repository.ts
2960
+ var ConversationRepository = class extends Repository {
2961
+ events;
2962
+ constructor(model, plugins, deps, pagination) {
2963
+ super(model, plugins, pagination);
2964
+ this.events = deps.events;
2965
+ }
2966
+ async openConversation(input, ctx) {
2967
+ const draft = {
2968
+ channel: input.channel,
2969
+ participantKind: input.participantKind,
2970
+ participantId: input.participantId,
2971
+ status: "open",
2972
+ unreadCount: 0,
2973
+ ...input.channelUserId !== void 0 ? { channelUserId: input.channelUserId } : {},
2974
+ ...input.assigneeId !== void 0 ? { assigneeId: input.assigneeId } : {},
2975
+ ...input.metadata !== void 0 ? { metadata: input.metadata } : {}
2976
+ };
2977
+ const created = await this.create(draft, ctx);
2978
+ await this.events.publish(createEvent(CRM_EVENTS.CONVERSATION_CREATED, {
2979
+ conversationId: String(created._id),
2980
+ channel: created.channel,
2981
+ participantKind: created.participantKind,
2982
+ participantId: String(created.participantId)
2983
+ }, ctx));
2984
+ return created;
2985
+ }
2986
+ async assign(id, assigneeId, ctx) {
2987
+ const before = await this.findOneAndUpdate({
2988
+ _id: id,
2989
+ assigneeId: { $ne: assigneeId }
2990
+ }, { $set: { assigneeId } }, {
2991
+ ...ctx,
2992
+ returnDocument: "before"
2993
+ });
2994
+ if (!before) {
2995
+ const current = await this.getById(id, ctx);
2996
+ if (!current) throw new NotFoundError("Conversation", id);
2997
+ return current;
2998
+ }
2999
+ await this.events.publish(createEvent(CRM_EVENTS.CONVERSATION_ASSIGNED, {
3000
+ conversationId: id,
3001
+ assigneeId,
3002
+ ...before.assigneeId !== void 0 ? { previousAssigneeId: before.assigneeId } : {}
3003
+ }, ctx));
3004
+ return await this.getById(id, ctx) ?? before;
3005
+ }
3006
+ async resolve(id, ctx) {
3007
+ return this._setStatus(id, "resolved", CRM_EVENTS.CONVERSATION_RESOLVED, ctx);
3008
+ }
3009
+ async reopen(id, ctx) {
3010
+ return this._setStatus(id, "open", CRM_EVENTS.CONVERSATION_REOPENED, ctx);
3011
+ }
3012
+ /** Reset unread badge. Pure UI affordance, no event. */
3013
+ async markRead(id, ctx) {
3014
+ const updated = await this.update(id, { unreadCount: 0 }, ctx);
3015
+ if (!updated) throw new NotFoundError("Conversation", id);
3016
+ return updated;
3017
+ }
3018
+ /**
3019
+ * Bump `lastMessageAt` and (for inbound) increment `unreadCount`.
3020
+ * Called by `MessageRepository` when a message is recorded — not part
3021
+ * of the public surface. Returns the conversation for the caller's
3022
+ * follow-up writes.
3023
+ */
3024
+ async touch(id, args, ctx) {
3025
+ const existing = await this.getById(id, ctx);
3026
+ if (!existing) return null;
3027
+ const patch = { lastMessageAt: args.sentAt };
3028
+ if (args.direction === "inbound") patch.unreadCount = (existing.unreadCount ?? 0) + 1;
3029
+ return await this.update(id, patch, ctx) ?? null;
3030
+ }
3031
+ async _setStatus(id, next, eventType, ctx) {
3032
+ const existing = await this.getById(id, ctx);
3033
+ if (!existing) throw new NotFoundError("Conversation", id);
3034
+ if (existing.status === next) return existing;
3035
+ const updated = await this.update(id, { status: next }, ctx);
3036
+ if (!updated) throw new NotFoundError("Conversation", id);
3037
+ await this.events.publish(createEvent(eventType, {
3038
+ conversationId: id,
3039
+ status: next,
3040
+ previousStatus: existing.status
3041
+ }, ctx));
3042
+ return updated;
3043
+ }
3044
+ };
3045
+ //#endregion
3046
+ //#region src/repositories/message.repository.ts
3047
+ var MessageRepository = class extends Repository {
3048
+ events;
3049
+ conversations;
3050
+ constructor(model, plugins, deps, pagination) {
3051
+ super(model, plugins, pagination);
3052
+ this.events = deps.events;
3053
+ this.conversations = deps.conversations;
3054
+ }
3055
+ async recordInbound(input, ctx) {
3056
+ return this._record(input, "inbound", "delivered", CRM_EVENTS.MESSAGE_RECEIVED, ctx);
3057
+ }
3058
+ async recordOutbound(input, ctx) {
3059
+ return this._record(input, "outbound", "sent", CRM_EVENTS.MESSAGE_SENT, ctx);
3060
+ }
3061
+ /** Wire delivery callback (e.g. WhatsApp `status` webhook). */
3062
+ async updateStatus(id, status, ctx) {
3063
+ const existing = await this.getById(id, ctx);
3064
+ if (!existing) throw new NotFoundError("Message", id);
3065
+ if (existing.status === status) return existing;
3066
+ const updated = await this.update(id, { status }, ctx);
3067
+ if (!updated) throw new NotFoundError("Message", id);
3068
+ return updated;
3069
+ }
3070
+ async _record(input, direction, defaultStatus, eventType, ctx) {
3071
+ const sentAt = input.sentAt ?? /* @__PURE__ */ new Date();
3072
+ const draft = {
3073
+ conversationId: input.conversationId,
3074
+ direction,
3075
+ body: input.body,
3076
+ attachments: input.attachments ?? [],
3077
+ status: defaultStatus,
3078
+ sentAt,
3079
+ ...input.externalMessageId !== void 0 ? { externalMessageId: input.externalMessageId } : {},
3080
+ ...input.senderRef !== void 0 ? { senderRef: input.senderRef } : {},
3081
+ ...input.recipientRef !== void 0 ? { recipientRef: input.recipientRef } : {},
3082
+ ...input.metadata !== void 0 ? { metadata: input.metadata } : {}
3083
+ };
3084
+ const created = await this.create(draft, ctx);
3085
+ await this.conversations.touch(input.conversationId, {
3086
+ direction,
3087
+ sentAt
3088
+ }, ctx);
3089
+ await this.events.publish(createEvent(eventType, {
3090
+ messageId: String(created._id),
3091
+ conversationId: input.conversationId,
3092
+ direction,
3093
+ ...input.externalMessageId !== void 0 ? { externalMessageId: input.externalMessageId } : {}
3094
+ }, ctx));
3095
+ return created;
3096
+ }
3097
+ /** Latest `limit` messages — lightweight projection for the activity feed. */
3098
+ async findRecentForFeed(limit, ctx) {
3099
+ return this.findAll({}, {
3100
+ ...ctx,
3101
+ sort: { sentAt: -1 },
3102
+ limit,
3103
+ select: {
3104
+ _id: 1,
3105
+ body: 1,
3106
+ direction: 1,
3107
+ conversationId: 1,
3108
+ sentAt: 1
3109
+ }
3110
+ });
3111
+ }
3112
+ };
3113
+ //#endregion
2272
3114
  //#region src/repositories/create-repositories.ts
2273
3115
  /**
2274
3116
  * Repository factory — composes the shared mongokit plugin stack and
@@ -2306,6 +3148,11 @@ function createCrmRepositories(models, deps) {
2306
3148
  const note = new NoteRepository(models.Note, plugins, { events }, pagination);
2307
3149
  const lossReason = new LossReasonRepository(models.LossReason, plugins, { events }, pagination);
2308
3150
  const emailEvent = new EmailEventRepository(models.EmailEvent, plugins, { events }, pagination);
3151
+ const conversation = new ConversationRepository(models.Conversation, plugins, { events }, pagination);
3152
+ const message = new MessageRepository(models.Message, plugins, {
3153
+ events,
3154
+ conversations: conversation
3155
+ }, pagination);
2309
3156
  return Object.freeze({
2310
3157
  pipeline,
2311
3158
  lead,
@@ -2317,46 +3164,29 @@ function createCrmRepositories(models, deps) {
2317
3164
  activity,
2318
3165
  note,
2319
3166
  lossReason,
2320
- emailEvent
3167
+ emailEvent,
3168
+ conversation,
3169
+ message
2321
3170
  });
2322
3171
  }
2323
3172
  //#endregion
2324
- //#region src/events/in-process-bus.ts
2325
- /**
2326
- * In-process event bus — default fallback when no `eventTransport` is
2327
- * provided to `createCrm`. Structurally identical to arc's
2328
- * `MemoryEventTransport`; pattern matching is delegated to primitives.
2329
- * Handler errors are swallowed — event emission never blocks writes.
2330
- */
2331
- var InProcessCrmBus = class {
2332
- name = "in-process-crm";
2333
- subs = [];
2334
- logger;
2335
- constructor(options) {
2336
- if (options?.logger) this.logger = options.logger;
2337
- }
2338
- async publish(event) {
2339
- const matching = this.subs.filter((s) => matchEventPattern(s.pattern, event.type));
2340
- for (const sub of matching) try {
2341
- await sub.handler(event);
2342
- } catch (err) {
2343
- this.logger?.error?.(`[crm] event handler error [${event.type}]`, { error: err instanceof Error ? err.message : String(err) });
2344
- }
2345
- }
2346
- async subscribe(pattern, handler) {
2347
- const sub = {
2348
- pattern,
2349
- handler
2350
- };
2351
- this.subs.push(sub);
2352
- return () => {
2353
- this.subs = this.subs.filter((s) => s !== sub);
2354
- };
2355
- }
2356
- async close() {
2357
- this.subs = [];
3173
+ //#region src/engine/ensure-ready.ts
3174
+ const REQUIRED_CAPABILITIES = [
3175
+ "upsert",
3176
+ "getOrCreate",
3177
+ "duplicateKeyError"
3178
+ ];
3179
+ function assertCrmCapabilities(probe, options = {}) {
3180
+ const caps = probe.capabilities;
3181
+ if (!caps) throw new Error("[crm] backend declares no `capabilities` descriptor (requires @classytic/repo-core 0.6 / mongokit 3.16+).");
3182
+ const missing = REQUIRED_CAPABILITIES.filter((flag) => caps[flag] !== true);
3183
+ if (missing.length > 0) throw new Error(`[crm] backend missing required capabilities: ${missing.join(", ")}. Lead dedup + conversion depend on them.`);
3184
+ if (caps.transactions !== true) {
3185
+ 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.";
3186
+ if (options.allowNonTransactional) options.logger?.warn?.(msg);
3187
+ else throw new Error(msg);
2358
3188
  }
2359
- };
3189
+ }
2360
3190
  //#endregion
2361
3191
  //#region src/engine/create-crm.ts
2362
3192
  async function createCrm(config) {
@@ -2365,6 +3195,8 @@ async function createCrm(config) {
2365
3195
  connection: config.connection,
2366
3196
  tenantFieldType: config.tenantFieldType ?? "objectId",
2367
3197
  multiTenant: config.multiTenant ?? true,
3198
+ forceRecreate: config.forceRecreate ?? false,
3199
+ autoIndex: config.autoIndex ?? true,
2368
3200
  ...config.logger !== void 0 ? { logger: config.logger } : {},
2369
3201
  ...config.pagination !== void 0 ? { pagination: config.pagination } : {},
2370
3202
  plugins: config.plugins ?? []
@@ -2375,11 +3207,18 @@ async function createCrm(config) {
2375
3207
  events,
2376
3208
  config: resolved
2377
3209
  });
3210
+ assertCrmCapabilities(repositories.lead, {
3211
+ ...config.allowNonTransactional !== void 0 ? { allowNonTransactional: config.allowNonTransactional } : {},
3212
+ ...config.logger?.warn !== void 0 ? { logger: { warn: (m) => config.logger?.warn?.(m) } } : {}
3213
+ });
2378
3214
  return Object.freeze({
2379
3215
  models,
2380
3216
  repositories,
2381
3217
  events,
2382
3218
  config: Object.freeze(resolved),
3219
+ async syncIndexes() {
3220
+ await Promise.all(Object.values(models).map((m) => m.syncIndexes()));
3221
+ },
2383
3222
  async dispose() {
2384
3223
  await events.close?.();
2385
3224
  }
@@ -2753,7 +3592,11 @@ const contactInfoSchema = z.object({
2753
3592
  email: z.string().email().max(255).optional(),
2754
3593
  personalEmail: z.string().email().max(255).optional(),
2755
3594
  phone: z.string().max(50).optional(),
2756
- alternatePhone: z.string().max(50).optional()
3595
+ alternatePhone: z.string().max(50).optional(),
3596
+ whatsapp: z.string().max(50).optional(),
3597
+ telegram: z.string().max(100).optional(),
3598
+ linkedin: z.string().max(255).optional(),
3599
+ instagram: z.string().max(100).optional()
2757
3600
  });
2758
3601
  const createContactSchema = z.object({
2759
3602
  name: contactNameSchema,
@@ -2967,6 +3810,4 @@ var validators_exports = /* @__PURE__ */ __exportAll({
2967
3810
  updatePipelineSchema: () => updatePipelineSchema
2968
3811
  });
2969
3812
  //#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
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 };