@classytic/crm 0.8.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +151 -0
- package/LICENSE +75 -21
- package/README.md +51 -2
- package/dist/events.d.ts +19 -12
- package/dist/events.js +41 -31
- package/dist/index.d.ts +505 -128
- package/dist/index.js +1120 -441
- package/package.json +15 -9
- package/scripts/lead-contact-email-key-drift.mjs +218 -0
- package/scripts/suppression-key-drift.mjs +143 -0
- /package/dist/{chunk.js → rolldown-runtime.js} +0 -0
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { ModelBlueprint, PaginationConfig, PluginType, Repository } from "@classytic/mongokit";
|
|
2
2
|
import { Connection, Model, Schema, SchemaDefinitionProperty } from "mongoose";
|
|
3
|
-
import { PaginationConfig, PluginType, Repository } from "@classytic/mongokit";
|
|
4
3
|
import { DomainEvent, EventHandler, EventMeta, EventTransport } from "@classytic/primitives/events";
|
|
5
|
-
import
|
|
4
|
+
import { InProcessEventBus } from "@classytic/primitives/event-infra";
|
|
6
5
|
import { z } from "zod";
|
|
7
6
|
import { StatusChangeEntry, StatusHistory, appendStatus, lastTransitionTo, latestEntry, timeInStatus } from "@classytic/primitives/status-history";
|
|
8
7
|
import { OperationContext } from "@classytic/primitives/context";
|
|
9
8
|
import { OutboxStore } from "@classytic/primitives/outbox";
|
|
10
|
-
|
|
9
|
+
import { HttpError } from "@classytic/repo-core/errors";
|
|
11
10
|
//#region src/events/dispatch.d.ts
|
|
12
11
|
/**
|
|
13
12
|
* Context key for the post-commit event queue. `Symbol.for` so the host
|
|
@@ -59,12 +58,39 @@ declare class CrmEventDispatcher implements EventTransport {
|
|
|
59
58
|
*/
|
|
60
59
|
declare function flushPending(transport: Pick<EventTransport, 'publish'> | undefined, events: DomainEvent[], logger?: CrmLogger): Promise<void>;
|
|
61
60
|
//#endregion
|
|
61
|
+
//#region src/engine/modules.d.ts
|
|
62
|
+
/** Coarse CRM feature bundles. Keep entity-level switches out of the API. */
|
|
63
|
+
interface CrmModulesConfig {
|
|
64
|
+
/** Operational sales CRM: leads, accounts, contacts, opportunities and follow-up. */
|
|
65
|
+
sales?: boolean | undefined;
|
|
66
|
+
/** Customer communication: conversations, messages, consent and email events. */
|
|
67
|
+
engagement?: boolean | undefined;
|
|
68
|
+
/** Campaign and sequence automation. Implies engagement and sales. */
|
|
69
|
+
automation?: boolean | undefined;
|
|
70
|
+
}
|
|
71
|
+
interface ResolvedCrmModules {
|
|
72
|
+
readonly sales: boolean;
|
|
73
|
+
readonly engagement: boolean;
|
|
74
|
+
readonly automation: boolean;
|
|
75
|
+
}
|
|
76
|
+
declare const ALL_CRM_MODULES: ResolvedCrmModules;
|
|
77
|
+
/**
|
|
78
|
+
* Resolve module dependencies once at boot.
|
|
79
|
+
*
|
|
80
|
+
* Omitting `modules` preserves the pre-module package behaviour (everything
|
|
81
|
+
* enabled). Supplying it is opt-in: unspecified modules are disabled.
|
|
82
|
+
*/
|
|
83
|
+
declare function resolveCrmModules(modules: CrmModulesConfig | undefined): ResolvedCrmModules;
|
|
84
|
+
//#endregion
|
|
62
85
|
//#region src/models/tenant-field.d.ts
|
|
63
86
|
type TenantFieldType = 'objectId' | 'string';
|
|
64
87
|
interface SchemaBuildConfig {
|
|
65
88
|
/** Schema type for the tenant column. Engine plumbs this everywhere. */
|
|
66
89
|
tenantFieldType: TenantFieldType;
|
|
90
|
+
/** Optional Mongoose model namespace for hosts with generic model names. */
|
|
91
|
+
modelPrefix?: string | undefined;
|
|
67
92
|
}
|
|
93
|
+
declare function crmModelName(config: SchemaBuildConfig, base: string): string;
|
|
68
94
|
/**
|
|
69
95
|
* Tenant-column schema definition. Use this anywhere a model has
|
|
70
96
|
* `organizationId` — never write the raw type in a schema file.
|
|
@@ -74,7 +100,7 @@ declare function tenantFieldDef(config: SchemaBuildConfig): SchemaDefinitionProp
|
|
|
74
100
|
//#region src/models/pipeline.schema.d.ts
|
|
75
101
|
/** Mongoose document shape for a single embedded stage. */
|
|
76
102
|
interface IStageDocument {
|
|
77
|
-
_id:
|
|
103
|
+
_id: import('mongoose').Types.ObjectId;
|
|
78
104
|
name: string;
|
|
79
105
|
/** Ascending order in the pipeline. Treated as identifier on `moveToStage`. */
|
|
80
106
|
sequence: number;
|
|
@@ -85,8 +111,8 @@ interface IStageDocument {
|
|
|
85
111
|
}
|
|
86
112
|
/** Mongoose document shape for a Pipeline. */
|
|
87
113
|
interface IPipelineDocument {
|
|
88
|
-
_id:
|
|
89
|
-
organizationId:
|
|
114
|
+
_id: import('mongoose').Types.ObjectId;
|
|
115
|
+
organizationId: import('mongoose').Types.ObjectId | string;
|
|
90
116
|
name: string;
|
|
91
117
|
/** Deprecates the pipeline — existing opps still move, no new ones created. */
|
|
92
118
|
isArchived: boolean;
|
|
@@ -138,8 +164,8 @@ interface ILeadEngagement {
|
|
|
138
164
|
lastEngagedAt?: Date;
|
|
139
165
|
}
|
|
140
166
|
interface ILeadDocument {
|
|
141
|
-
_id:
|
|
142
|
-
organizationId:
|
|
167
|
+
_id: import('mongoose').Types.ObjectId;
|
|
168
|
+
organizationId: import('mongoose').Types.ObjectId | string;
|
|
143
169
|
fullName: string;
|
|
144
170
|
firstName?: string;
|
|
145
171
|
lastName?: string;
|
|
@@ -155,11 +181,13 @@ interface ILeadDocument {
|
|
|
155
181
|
statusHistory: ILeadStatusEntry[];
|
|
156
182
|
ownerId?: string;
|
|
157
183
|
tags: string[];
|
|
158
|
-
convertedContactId?:
|
|
159
|
-
convertedAccountId?:
|
|
160
|
-
convertedOpportunityId?:
|
|
184
|
+
convertedContactId?: import('mongoose').Types.ObjectId | string;
|
|
185
|
+
convertedAccountId?: import('mongoose').Types.ObjectId | string;
|
|
186
|
+
convertedOpportunityId?: import('mongoose').Types.ObjectId | string;
|
|
161
187
|
convertedAt?: Date;
|
|
162
188
|
disqualifyReason?: string;
|
|
189
|
+
/** GDPR tombstone — set by `anonymize()` when PII was scrubbed. */
|
|
190
|
+
anonymizedAt?: Date;
|
|
163
191
|
metadata?: Record<string, unknown>;
|
|
164
192
|
createdAt: Date;
|
|
165
193
|
updatedAt: Date;
|
|
@@ -189,12 +217,12 @@ interface IOpportunityStatusEntry {
|
|
|
189
217
|
note?: string;
|
|
190
218
|
}
|
|
191
219
|
interface IOpportunityDocument {
|
|
192
|
-
_id:
|
|
193
|
-
organizationId:
|
|
220
|
+
_id: import('mongoose').Types.ObjectId;
|
|
221
|
+
organizationId: import('mongoose').Types.ObjectId | string;
|
|
194
222
|
name: string;
|
|
195
|
-
accountId?:
|
|
196
|
-
primaryContactId?:
|
|
197
|
-
pipelineId:
|
|
223
|
+
accountId?: import('mongoose').Types.ObjectId | string;
|
|
224
|
+
primaryContactId?: import('mongoose').Types.ObjectId | string;
|
|
225
|
+
pipelineId: import('mongoose').Types.ObjectId | string;
|
|
198
226
|
stageId: string;
|
|
199
227
|
status: OpportunityStatus;
|
|
200
228
|
statusHistory: IOpportunityStatusEntry[];
|
|
@@ -203,9 +231,9 @@ interface IOpportunityDocument {
|
|
|
203
231
|
probability: number;
|
|
204
232
|
expectedCloseAt?: Date;
|
|
205
233
|
closedAt?: Date;
|
|
206
|
-
lostReasonId?:
|
|
234
|
+
lostReasonId?: import('mongoose').Types.ObjectId | string;
|
|
207
235
|
ownerId?: string;
|
|
208
|
-
sourceLeadId?:
|
|
236
|
+
sourceLeadId?: import('mongoose').Types.ObjectId | string;
|
|
209
237
|
tags: string[];
|
|
210
238
|
metadata?: Record<string, unknown>;
|
|
211
239
|
createdAt: Date;
|
|
@@ -234,15 +262,17 @@ interface IContactInfoEmbedded {
|
|
|
234
262
|
instagram?: string;
|
|
235
263
|
}
|
|
236
264
|
interface IContactDocument {
|
|
237
|
-
_id:
|
|
238
|
-
organizationId:
|
|
265
|
+
_id: import('mongoose').Types.ObjectId;
|
|
266
|
+
organizationId: import('mongoose').Types.ObjectId | string;
|
|
239
267
|
name: IContactName;
|
|
240
268
|
contact: IContactInfoEmbedded;
|
|
241
269
|
jobTitle?: string;
|
|
242
|
-
accountId?:
|
|
270
|
+
accountId?: import('mongoose').Types.ObjectId | string;
|
|
243
271
|
ownerId?: string;
|
|
244
272
|
tags: string[];
|
|
245
273
|
metadata?: Record<string, unknown>;
|
|
274
|
+
/** GDPR tombstone — set by `anonymize()` when PII was scrubbed. */
|
|
275
|
+
anonymizedAt?: Date;
|
|
246
276
|
createdAt: Date;
|
|
247
277
|
updatedAt: Date;
|
|
248
278
|
}
|
|
@@ -251,8 +281,8 @@ declare function createContactModel(connection: Connection, config: SchemaBuildC
|
|
|
251
281
|
//#endregion
|
|
252
282
|
//#region src/models/account.schema.d.ts
|
|
253
283
|
interface IAccountDocument {
|
|
254
|
-
_id:
|
|
255
|
-
organizationId:
|
|
284
|
+
_id: import('mongoose').Types.ObjectId;
|
|
285
|
+
organizationId: import('mongoose').Types.ObjectId | string;
|
|
256
286
|
name: string;
|
|
257
287
|
domain?: string;
|
|
258
288
|
industry?: string;
|
|
@@ -291,8 +321,8 @@ interface ICampaignStatusEntry {
|
|
|
291
321
|
note?: string;
|
|
292
322
|
}
|
|
293
323
|
interface ICampaignDocument {
|
|
294
|
-
_id:
|
|
295
|
-
organizationId:
|
|
324
|
+
_id: import('mongoose').Types.ObjectId;
|
|
325
|
+
organizationId: import('mongoose').Types.ObjectId | string;
|
|
296
326
|
name: string;
|
|
297
327
|
description?: string;
|
|
298
328
|
status: CampaignStatus;
|
|
@@ -342,10 +372,10 @@ interface ISubscriptionStatusEntry {
|
|
|
342
372
|
note?: string;
|
|
343
373
|
}
|
|
344
374
|
interface ISubscriptionDocument {
|
|
345
|
-
_id:
|
|
346
|
-
organizationId:
|
|
347
|
-
contactId:
|
|
348
|
-
campaignId:
|
|
375
|
+
_id: import('mongoose').Types.ObjectId;
|
|
376
|
+
organizationId: import('mongoose').Types.ObjectId | string;
|
|
377
|
+
contactId: import('mongoose').Types.ObjectId | string;
|
|
378
|
+
campaignId: import('mongoose').Types.ObjectId | string;
|
|
349
379
|
status: SubscriptionStatus;
|
|
350
380
|
statusHistory: ISubscriptionStatusEntry[];
|
|
351
381
|
source?: string;
|
|
@@ -376,9 +406,17 @@ type SuppressionReason = (typeof SUPPRESSION_REASONS)[number];
|
|
|
376
406
|
//#endregion
|
|
377
407
|
//#region src/models/suppression.schema.d.ts
|
|
378
408
|
interface ISuppressionDocument {
|
|
379
|
-
_id:
|
|
380
|
-
organizationId:
|
|
381
|
-
/**
|
|
409
|
+
_id: import('mongoose').Types.ObjectId;
|
|
410
|
+
organizationId: import('mongoose').Types.ObjectId | string;
|
|
411
|
+
/**
|
|
412
|
+
* The CANONICAL address, as `@classytic/contact/email` defines it: trimmed,
|
|
413
|
+
* both sides lowercased, and the domain IDNA-folded to punycode. The
|
|
414
|
+
* repository canonicalises before every read and write — the schema's
|
|
415
|
+
* `lowercase: true` is a backstop for a direct model write, not the rule.
|
|
416
|
+
*
|
|
417
|
+
* `ada@bücher.de` is stored as `ada@xn--bcher-kva.de`. Two spellings of one
|
|
418
|
+
* host stored as two rows is a suppression that silently never matches.
|
|
419
|
+
*/
|
|
382
420
|
email: string;
|
|
383
421
|
reason: SuppressionReason;
|
|
384
422
|
/** Free-form origin: 'campaign:<id>', 'sequence:<id>', 'api', 'import', … */
|
|
@@ -415,12 +453,12 @@ type SubjectKind = (typeof SUBJECT_KINDS)[number];
|
|
|
415
453
|
//#endregion
|
|
416
454
|
//#region src/models/activity.schema.d.ts
|
|
417
455
|
interface IActivityDocument {
|
|
418
|
-
_id:
|
|
419
|
-
organizationId:
|
|
456
|
+
_id: import('mongoose').Types.ObjectId;
|
|
457
|
+
organizationId: import('mongoose').Types.ObjectId | string;
|
|
420
458
|
type: ActivityType;
|
|
421
459
|
status: ActivityStatus;
|
|
422
460
|
subjectKind: SubjectKind;
|
|
423
|
-
subjectId:
|
|
461
|
+
subjectId: import('mongoose').Types.ObjectId | string;
|
|
424
462
|
title: string;
|
|
425
463
|
description?: string;
|
|
426
464
|
ownerId?: string;
|
|
@@ -437,10 +475,10 @@ declare function createActivityModel(connection: Connection, config: SchemaBuild
|
|
|
437
475
|
//#endregion
|
|
438
476
|
//#region src/models/note.schema.d.ts
|
|
439
477
|
interface INoteDocument {
|
|
440
|
-
_id:
|
|
441
|
-
organizationId:
|
|
478
|
+
_id: import('mongoose').Types.ObjectId;
|
|
479
|
+
organizationId: import('mongoose').Types.ObjectId | string;
|
|
442
480
|
subjectKind: SubjectKind;
|
|
443
|
-
subjectId:
|
|
481
|
+
subjectId: import('mongoose').Types.ObjectId | string;
|
|
444
482
|
body: string;
|
|
445
483
|
authorId?: string;
|
|
446
484
|
pinned: boolean;
|
|
@@ -453,8 +491,8 @@ declare function createNoteModel(connection: Connection, config: SchemaBuildConf
|
|
|
453
491
|
//#endregion
|
|
454
492
|
//#region src/models/loss-reason.schema.d.ts
|
|
455
493
|
interface ILossReasonDocument {
|
|
456
|
-
_id:
|
|
457
|
-
organizationId:
|
|
494
|
+
_id: import('mongoose').Types.ObjectId;
|
|
495
|
+
organizationId: import('mongoose').Types.ObjectId | string;
|
|
458
496
|
name: string;
|
|
459
497
|
description?: string;
|
|
460
498
|
isActive: boolean;
|
|
@@ -501,12 +539,12 @@ type EmailEventKind = (typeof EMAIL_EVENT_KINDS)[number];
|
|
|
501
539
|
declare const EMAIL_EVENT_SUBJECT_KINDS: readonly ["contact", "subscription"];
|
|
502
540
|
type EmailEventSubjectKind = (typeof EMAIL_EVENT_SUBJECT_KINDS)[number];
|
|
503
541
|
interface IEmailEventDocument {
|
|
504
|
-
_id:
|
|
505
|
-
organizationId:
|
|
542
|
+
_id: import('mongoose').Types.ObjectId;
|
|
543
|
+
organizationId: import('mongoose').Types.ObjectId | string;
|
|
506
544
|
kind: EmailEventKind;
|
|
507
545
|
subjectKind: EmailEventSubjectKind;
|
|
508
|
-
subjectId:
|
|
509
|
-
campaignId?:
|
|
546
|
+
subjectId: import('mongoose').Types.ObjectId | string;
|
|
547
|
+
campaignId?: import('mongoose').Types.ObjectId | string;
|
|
510
548
|
messageId?: string;
|
|
511
549
|
url?: string;
|
|
512
550
|
userAgent?: string;
|
|
@@ -540,11 +578,11 @@ type MessageDeliveryStatus = (typeof MESSAGE_DELIVERY_STATUSES)[number];
|
|
|
540
578
|
//#endregion
|
|
541
579
|
//#region src/models/conversation.schema.d.ts
|
|
542
580
|
interface IConversationDocument {
|
|
543
|
-
_id:
|
|
544
|
-
organizationId:
|
|
581
|
+
_id: import('mongoose').Types.ObjectId;
|
|
582
|
+
organizationId: import('mongoose').Types.ObjectId | string;
|
|
545
583
|
channel: ConversationChannel;
|
|
546
584
|
participantKind: ConversationParticipantKind;
|
|
547
|
-
participantId:
|
|
585
|
+
participantId: import('mongoose').Types.ObjectId | string;
|
|
548
586
|
/** Channel-side identifier (e.g. WhatsApp wa_id, Telegram chat id). */
|
|
549
587
|
channelUserId?: string;
|
|
550
588
|
status: ConversationStatus;
|
|
@@ -571,9 +609,9 @@ interface IMessageAttachment {
|
|
|
571
609
|
mimeType?: string;
|
|
572
610
|
}
|
|
573
611
|
interface IMessageDocument {
|
|
574
|
-
_id:
|
|
575
|
-
organizationId:
|
|
576
|
-
conversationId:
|
|
612
|
+
_id: import('mongoose').Types.ObjectId;
|
|
613
|
+
organizationId: import('mongoose').Types.ObjectId | string;
|
|
614
|
+
conversationId: import('mongoose').Types.ObjectId | string;
|
|
577
615
|
direction: MessageDirection;
|
|
578
616
|
body: string;
|
|
579
617
|
attachments: IMessageAttachment[];
|
|
@@ -635,8 +673,8 @@ interface ISequenceStatusEntry {
|
|
|
635
673
|
note?: string;
|
|
636
674
|
}
|
|
637
675
|
interface ISequenceDocument {
|
|
638
|
-
_id:
|
|
639
|
-
organizationId:
|
|
676
|
+
_id: import('mongoose').Types.ObjectId;
|
|
677
|
+
organizationId: import('mongoose').Types.ObjectId | string;
|
|
640
678
|
name: string;
|
|
641
679
|
description?: string;
|
|
642
680
|
status: SequenceStatus;
|
|
@@ -687,11 +725,11 @@ interface IEnrollmentStatusEntry {
|
|
|
687
725
|
by?: string;
|
|
688
726
|
}
|
|
689
727
|
interface IEnrollmentDocument {
|
|
690
|
-
_id:
|
|
691
|
-
organizationId:
|
|
692
|
-
sequenceId:
|
|
728
|
+
_id: import('mongoose').Types.ObjectId;
|
|
729
|
+
organizationId: import('mongoose').Types.ObjectId | string;
|
|
730
|
+
sequenceId: import('mongoose').Types.ObjectId | string;
|
|
693
731
|
subjectKind: EnrollmentSubjectKind;
|
|
694
|
-
subjectId:
|
|
732
|
+
subjectId: import('mongoose').Types.ObjectId | string;
|
|
695
733
|
status: EnrollmentStatus;
|
|
696
734
|
/** Index into sequence.steps[] — 0-based. */
|
|
697
735
|
currentStepIndex: number;
|
|
@@ -730,19 +768,27 @@ interface CrmModels {
|
|
|
730
768
|
Sequence: Model<ISequenceDocument>;
|
|
731
769
|
Enrollment: Model<IEnrollmentDocument>;
|
|
732
770
|
}
|
|
733
|
-
|
|
771
|
+
type ModularCrmModels = Partial<CrmModels>;
|
|
772
|
+
/** Mongoose model names this package can register (before `modelPrefix`). Reference/documentation. */
|
|
734
773
|
declare const CRM_MODEL_NAMES: readonly ["Pipeline", "Lead", "Opportunity", "Contact", "Account", "Campaign", "EmailSubscription", "Suppression", "Activity", "Note", "LossReason", "EmailEvent", "Conversation", "Message", "Sequence", "Enrollment"];
|
|
735
|
-
interface
|
|
774
|
+
interface DefineCrmModelsConfig extends SchemaBuildConfig {
|
|
775
|
+
/** Which model families to describe. A disabled family contributes no spec. */
|
|
776
|
+
modules: ResolvedCrmModules;
|
|
736
777
|
/**
|
|
737
|
-
* On a model-name collision: `false` (default)
|
|
738
|
-
* `
|
|
739
|
-
*
|
|
778
|
+
* On a model-name collision: `false` (default) uses mongokit's `throw`
|
|
779
|
+
* policy (`ModelCollisionError`); `true` uses `replace` (test fixtures /
|
|
780
|
+
* hot reload only — mongokit refuses `replace` under `NODE_ENV=production`).
|
|
740
781
|
*/
|
|
741
782
|
forceRecreate?: boolean;
|
|
742
|
-
/** Mongoose auto-index builds. Default `true` (dev); `false` in prod. */
|
|
783
|
+
/** Mongoose auto-index builds. Default `true` (dev); `false` in prod (§35). */
|
|
743
784
|
autoIndex?: boolean;
|
|
744
785
|
}
|
|
745
|
-
|
|
786
|
+
/**
|
|
787
|
+
* Describe the CRM model set as a connection-free blueprint. Pure — builds no
|
|
788
|
+
* schema and registers nothing until `.bind(connection)`. Only the enabled
|
|
789
|
+
* module families contribute `ModelSpec` entries.
|
|
790
|
+
*/
|
|
791
|
+
declare function defineCrmModels(config: DefineCrmModelsConfig): ModelBlueprint<ModularCrmModels>;
|
|
746
792
|
//#endregion
|
|
747
793
|
//#region src/repositories/pipeline.repository.d.ts
|
|
748
794
|
interface PipelineRepositoryDeps {
|
|
@@ -809,6 +855,11 @@ declare class LeadRepository extends Repository<ILeadDocument> {
|
|
|
809
855
|
* Create a Lead. Auto-scores on insert so hosts can route immediately.
|
|
810
856
|
* Rejects duplicate emails inside the same tenant unless
|
|
811
857
|
* `rejectDuplicates: false` was set at engine boot.
|
|
858
|
+
*
|
|
859
|
+
* Throws `LeadEmailFormatError` (400) on an email that cannot be canonicalised
|
|
860
|
+
* — nothing is written. The reason lives beside the helper in
|
|
861
|
+
* `core/email-identity.ts`, which also explains why the SCHEMA setter degrades
|
|
862
|
+
* where this verb refuses.
|
|
812
863
|
*/
|
|
813
864
|
createLead(input: Partial<ILeadDocument>, ctx?: CrmContext): Promise<ILeadDocument>;
|
|
814
865
|
markContacted(id: string, extras?: TransitionExtras, ctx?: CrmContext): Promise<ILeadDocument>;
|
|
@@ -845,6 +896,29 @@ declare class LeadRepository extends Repository<ILeadDocument> {
|
|
|
845
896
|
* automatically; hosts also call this after enrichment writes.
|
|
846
897
|
*/
|
|
847
898
|
recomputeScore(id: string, ctx?: CrmContext): Promise<ILeadDocument>;
|
|
899
|
+
/**
|
|
900
|
+
* The `converted` hop, owned by `LeadService.convert()` (the only caller
|
|
901
|
+
* — the four cross-repo writes make it a service concern). Kept on the
|
|
902
|
+
* repo so the atomic state write goes through `applyTransition` — a
|
|
903
|
+
* machine-guarded CAS that `$push`es the history entry instead of
|
|
904
|
+
* read-then-replacing the whole `statusHistory` array (no lost-update
|
|
905
|
+
* window). Deliberately emits NO `lead.status_changed`: `lead.converted`
|
|
906
|
+
* is the canonical signal for this transition and is dispatched by the
|
|
907
|
+
* service alongside the cross-repo refs.
|
|
908
|
+
*/
|
|
909
|
+
markConverted(id: string, from: LeadStatus, patch: Partial<ILeadDocument>, entry: ILeadStatusEntry, ctx?: CrmContext): Promise<ILeadDocument>;
|
|
910
|
+
/**
|
|
911
|
+
* GDPR right-to-erasure for a Lead. Scrubs the PII fields
|
|
912
|
+
* (`fullName`/`firstName`/`lastName`/`email`/`phone`) in place while
|
|
913
|
+
* preserving non-PII aggregates (`score`, `status`, `statusHistory`,
|
|
914
|
+
* `engagement`, firmographic `companyName`/`source`) and the id. Sets
|
|
915
|
+
* `anonymizedAt` and emits `crm:lead.anonymized`.
|
|
916
|
+
*
|
|
917
|
+
* `fullName` is `required`, so it becomes the `'[redacted]'` sentinel;
|
|
918
|
+
* the optional PII scalars are nulled (dropping `email` out of the
|
|
919
|
+
* partial-unique email index). Idempotent.
|
|
920
|
+
*/
|
|
921
|
+
anonymize(id: string, ctx?: CrmContext): Promise<ILeadDocument>;
|
|
848
922
|
private _transition;
|
|
849
923
|
/**
|
|
850
924
|
* Count leads created on or after `since`. Used by dashboard KPI tiles.
|
|
@@ -937,15 +1011,21 @@ declare class OpportunityRepository extends Repository<IOpportunityDocument> {
|
|
|
937
1011
|
/** Count opportunities with status `open`. Used by dashboard KPI tiles. */
|
|
938
1012
|
countOpen(ctx?: CrmContext): Promise<number>;
|
|
939
1013
|
/**
|
|
940
|
-
* Sum `amount` on won opportunities updated
|
|
941
|
-
*
|
|
942
|
-
*
|
|
943
|
-
*
|
|
1014
|
+
* Sum `amount` on won opportunities updated on/after `since`, grouped by
|
|
1015
|
+
* currency. Returns one row per currency (descending by total) — a deal
|
|
1016
|
+
* book is multi-currency, so collapsing to a single total would silently
|
|
1017
|
+
* drop every non-top currency's revenue.
|
|
1018
|
+
*
|
|
1019
|
+
* Uses `aggregatePipeline` (a DB-side `$group`) instead of loading every
|
|
1020
|
+
* won opp into memory: the multiTenant + soft-delete policy hooks prepend
|
|
1021
|
+
* their `$match` so a missing ctx can't leak other tenants' revenue, and
|
|
1022
|
+
* the row count is bounded by the number of distinct currencies, not the
|
|
1023
|
+
* number of deals.
|
|
944
1024
|
*/
|
|
945
|
-
sumWonSince(since: Date, ctx?: CrmContext): Promise<{
|
|
1025
|
+
sumWonSince(since: Date, ctx?: CrmContext): Promise<Array<{
|
|
1026
|
+
currency: string;
|
|
946
1027
|
total: number;
|
|
947
|
-
|
|
948
|
-
} | null>;
|
|
1028
|
+
}>>;
|
|
949
1029
|
/** Latest `limit` opportunities — lightweight projection for the activity feed. */
|
|
950
1030
|
findRecentForFeed(limit: number, ctx?: CrmContext): Promise<IOpportunityDocument[]>;
|
|
951
1031
|
}
|
|
@@ -953,7 +1033,23 @@ declare class OpportunityRepository extends Repository<IOpportunityDocument> {
|
|
|
953
1033
|
//#region src/repositories/contact.repository.d.ts
|
|
954
1034
|
interface ContactRepositoryDeps {
|
|
955
1035
|
events: CrmEventDispatcher;
|
|
1036
|
+
/** Reject `createContact()` when an email already exists in-tenant (default true). */
|
|
1037
|
+
rejectDuplicates?: boolean;
|
|
956
1038
|
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Optional synchronous re-point hook fired INSIDE `merge()`, BEFORE the
|
|
1041
|
+
* secondary is deleted — inside the caller's `ctx.session` when one is
|
|
1042
|
+
* passed. Use it to move the doomed secondary's FKs onto the primary
|
|
1043
|
+
* (EmailSubscription
|
|
1044
|
+
* `.contactId` is `required`, plus Opportunity `.primaryContactId`,
|
|
1045
|
+
* Activity/Note `.subjectId`) so the delete never orphans a required
|
|
1046
|
+
* reference. Throwing here aborts the merge — nothing is deleted.
|
|
1047
|
+
*/
|
|
1048
|
+
type ContactMergeRepoint = (args: {
|
|
1049
|
+
primaryId: string;
|
|
1050
|
+
secondaryId: string;
|
|
1051
|
+
ctx?: CrmContext;
|
|
1052
|
+
}) => Promise<void> | void;
|
|
957
1053
|
interface MergeContactInput {
|
|
958
1054
|
/** Surviving contact id. Receives merged fields. */
|
|
959
1055
|
primaryId: string;
|
|
@@ -965,13 +1061,30 @@ interface MergeContactInput {
|
|
|
965
1061
|
* for the reverse. `tags` are always unioned.
|
|
966
1062
|
*/
|
|
967
1063
|
strategy?: 'preferPrimary' | 'preferSecondary';
|
|
1064
|
+
/**
|
|
1065
|
+
* Optional FK re-point seam — see {@link ContactMergeRepoint}. Pass this
|
|
1066
|
+
* to atomically move the secondary's required references onto the primary
|
|
1067
|
+
* before it is deleted. When omitted, re-pointing stays the host's job
|
|
1068
|
+
* via the async `crm:contact.merged` event (the legacy contract).
|
|
1069
|
+
*/
|
|
1070
|
+
repoint?: ContactMergeRepoint;
|
|
968
1071
|
}
|
|
969
1072
|
declare class ContactRepository extends Repository<IContactDocument> {
|
|
970
1073
|
private readonly events;
|
|
1074
|
+
private readonly rejectDuplicates;
|
|
971
1075
|
constructor(model: Model<IContactDocument>, plugins: PluginType[], deps: ContactRepositoryDeps, pagination?: PaginationConfig);
|
|
972
1076
|
/**
|
|
973
1077
|
* Create + emit `crm:contact.created`. Hosts call this instead of
|
|
974
1078
|
* `create()` when they want the event side-effect (e.g. webhook fan-out).
|
|
1079
|
+
*
|
|
1080
|
+
* Rejects duplicate emails inside the same tenant unless
|
|
1081
|
+
* `rejectDuplicates: false` was set — mirrors `LeadRepository.createLead`.
|
|
1082
|
+
* The partial-unique `(organizationId, contact.email)` index is the
|
|
1083
|
+
* authoritative guard; the pre-check only closes the fast path.
|
|
1084
|
+
*
|
|
1085
|
+
* Throws `ContactEmailFormatError` (400) on an email that cannot be
|
|
1086
|
+
* canonicalised — nothing is written. See `core/email-identity.ts` for the
|
|
1087
|
+
* reason, including what it means for `LeadService.convert()`.
|
|
975
1088
|
*/
|
|
976
1089
|
createContact(input: Partial<IContactDocument>, ctx?: CrmContext): Promise<IContactDocument>;
|
|
977
1090
|
/**
|
|
@@ -997,6 +1110,22 @@ declare class ContactRepository extends Repository<IContactDocument> {
|
|
|
997
1110
|
* from there.
|
|
998
1111
|
*/
|
|
999
1112
|
merge(input: MergeContactInput, ctx?: CrmContext): Promise<IContactDocument>;
|
|
1113
|
+
/**
|
|
1114
|
+
* GDPR right-to-erasure. Scrubs every PII field on the contact in place —
|
|
1115
|
+
* the structured `name` and ALL `contact.*` channels (email,
|
|
1116
|
+
* personalEmail, phone, alternatePhone, whatsapp, telegram, linkedin,
|
|
1117
|
+
* instagram) — while preserving the id and non-PII aggregates
|
|
1118
|
+
* (`accountId`, `ownerId`, `tags`, timestamps) so historical rollups and
|
|
1119
|
+
* relationships survive. Sets `anonymizedAt` as a tombstone and emits
|
|
1120
|
+
* `crm:contact.anonymized`. Idempotent: a second call re-scrubs to the
|
|
1121
|
+
* same sentinel.
|
|
1122
|
+
*
|
|
1123
|
+
* `name.given` is `required`, so it becomes the `'[redacted]'` sentinel
|
|
1124
|
+
* rather than empty; every other name part and the whole `contact`
|
|
1125
|
+
* subdocument are cleared. Unsetting `contact.email` also drops the row
|
|
1126
|
+
* out of the partial-unique email index, freeing the address for reuse.
|
|
1127
|
+
*/
|
|
1128
|
+
anonymize(id: string, ctx?: CrmContext): Promise<IContactDocument>;
|
|
1000
1129
|
}
|
|
1001
1130
|
//#endregion
|
|
1002
1131
|
//#region src/repositories/account.repository.d.ts
|
|
@@ -1084,6 +1213,15 @@ interface SuppressInput {
|
|
|
1084
1213
|
subjectId?: string;
|
|
1085
1214
|
metadata?: Record<string, unknown>;
|
|
1086
1215
|
}
|
|
1216
|
+
/**
|
|
1217
|
+
* The suppression key for `email`, or `null` when it cannot be canonicalised.
|
|
1218
|
+
*
|
|
1219
|
+
* Exported because it is the ONLY way a caller can distinguish "this address is on
|
|
1220
|
+
* the list" from "this address is not an address" — the read verbs collapse both
|
|
1221
|
+
* to `true` on purpose (see the fail-closed rule in the module docblock), and a
|
|
1222
|
+
* caller that needs the distinction should not re-derive the rule to get it.
|
|
1223
|
+
*/
|
|
1224
|
+
declare function suppressionEmailKey(email: string): string | null;
|
|
1087
1225
|
declare class SuppressionRepository extends Repository<ISuppressionDocument> {
|
|
1088
1226
|
private readonly events;
|
|
1089
1227
|
constructor(model: Model<ISuppressionDocument>, plugins: PluginType[], deps: SuppressionRepositoryDeps, pagination?: PaginationConfig);
|
|
@@ -1092,18 +1230,36 @@ declare class SuppressionRepository extends Repository<ISuppressionDocument> {
|
|
|
1092
1230
|
* `getOrCreate` on the unique (org, email) index — of N concurrent
|
|
1093
1231
|
* calls exactly one inserts. Emits `crm:suppression.added` only for
|
|
1094
1232
|
* the inserting call; an existing row is returned unchanged, no event.
|
|
1233
|
+
*
|
|
1234
|
+
* Throws `SuppressionEmailFormatError` (400) on an address that cannot be
|
|
1235
|
+
* canonicalised — nothing is written.
|
|
1095
1236
|
*/
|
|
1096
1237
|
suppress(email: string, input: SuppressInput, ctx?: CrmContext): Promise<ISuppressionDocument>;
|
|
1097
|
-
/**
|
|
1238
|
+
/**
|
|
1239
|
+
* True when `email` is on the org's suppression list — **or when it cannot be
|
|
1240
|
+
* canonicalised at all**, which is the fail-closed half of the module rule
|
|
1241
|
+
* above. Never throws on a malformed address: the safe answer is "do not send",
|
|
1242
|
+
* and this verb's job is to produce that answer, not to police the input.
|
|
1243
|
+
*/
|
|
1098
1244
|
isSuppressed(email: string, ctx?: CrmContext): Promise<boolean>;
|
|
1099
1245
|
/**
|
|
1100
|
-
* Return the suppressed subset of `emails`
|
|
1101
|
-
*
|
|
1246
|
+
* Return the suppressed subset of `emails` as a Set — ONE `$in` query, built for
|
|
1247
|
+
* the campaign-fanout hot path:
|
|
1102
1248
|
*
|
|
1103
1249
|
* ```ts
|
|
1104
1250
|
* const blocked = await suppression.filterSuppressed(recipients, ctx);
|
|
1105
|
-
* const sendable = recipients.filter((e) => !blocked.has(e
|
|
1251
|
+
* const sendable = recipients.filter((e) => !blocked.has(suppressionEmailKey(e) ?? e));
|
|
1106
1252
|
* ```
|
|
1253
|
+
*
|
|
1254
|
+
* The Set is keyed CANONICALLY, so a caller must canonicalise its own recipient
|
|
1255
|
+
* before testing membership — matching on the raw spelling is how the punycode
|
|
1256
|
+
* miss came back. `suppressionEmailKey` is exported for exactly that, and returns
|
|
1257
|
+
* `null` for an address with no canonical form.
|
|
1258
|
+
*
|
|
1259
|
+
* An address that cannot be canonicalised is reported as BLOCKED, keyed by the
|
|
1260
|
+
* caller's original spelling (there is no canonical form to key it by, and the
|
|
1261
|
+
* caller has to be able to find it). It is not thrown: one malformed recipient
|
|
1262
|
+
* must not abort a fanout to thousands of valid ones.
|
|
1107
1263
|
*/
|
|
1108
1264
|
filterSuppressed(emails: string[], ctx?: CrmContext): Promise<Set<string>>;
|
|
1109
1265
|
/**
|
|
@@ -1111,6 +1267,11 @@ declare class SuppressionRepository extends Repository<ISuppressionDocument> {
|
|
|
1111
1267
|
* `deleteMany` on the (unique) email key — emits
|
|
1112
1268
|
* `crm:suppression.removed` only when a row was actually removed.
|
|
1113
1269
|
* Returns whether a row was removed.
|
|
1270
|
+
*
|
|
1271
|
+
* Throws `SuppressionEmailFormatError` (400) on an address that cannot be
|
|
1272
|
+
* canonicalised, rather than answering `false`. This is the verb that re-enables
|
|
1273
|
+
* sending, and `false` would read as "already removed" when the truth is "that
|
|
1274
|
+
* was never an address" — an operator would close the ticket either way.
|
|
1114
1275
|
*/
|
|
1115
1276
|
unsuppress(email: string, ctx?: CrmContext): Promise<boolean>;
|
|
1116
1277
|
}
|
|
@@ -1342,56 +1503,78 @@ interface CrmLogger {
|
|
|
1342
1503
|
error?(msg: string, meta?: Record<string, unknown>): void;
|
|
1343
1504
|
debug?(msg: string, meta?: Record<string, unknown>): void;
|
|
1344
1505
|
}
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1506
|
+
/**
|
|
1507
|
+
* DESCRIBE-time shape (Kernel Construction Standard §6.2) — everything that
|
|
1508
|
+
* determines the persistence SHAPE and needs no connection or I/O to inspect.
|
|
1509
|
+
* Passed to `defineCrm(shape)`; nothing here opens a socket or reads a secret.
|
|
1510
|
+
*/
|
|
1511
|
+
interface CrmShape {
|
|
1348
1512
|
/**
|
|
1349
1513
|
* Tenant column shape. `'objectId'` (default) matches Better Auth orgs
|
|
1350
1514
|
* and unlocks `$lookup` / `.populate()`; `'string'` matches UUID hosts.
|
|
1351
1515
|
* Threaded into both schemas and `multiTenantPlugin` so they stay in
|
|
1352
1516
|
* sync (PACKAGE_RULES §9.2).
|
|
1353
1517
|
*/
|
|
1354
|
-
tenantFieldType?: TenantFieldType;
|
|
1518
|
+
tenantFieldType?: TenantFieldType | undefined;
|
|
1519
|
+
/**
|
|
1520
|
+
* Prefix for Mongoose model registry names (for example `Crm` produces
|
|
1521
|
+
* `CrmAccount`). Use in large hosts where generic names such as `Account`
|
|
1522
|
+
* are already owned by accounting or identity modules.
|
|
1523
|
+
*/
|
|
1524
|
+
modelPrefix?: string | undefined;
|
|
1355
1525
|
/**
|
|
1356
1526
|
* When `true` (default), wire `multiTenantPlugin` on every repository
|
|
1357
1527
|
* with `required: true`. Set `false` only for fixtures / smoke tests
|
|
1358
1528
|
* that intentionally drop the scope.
|
|
1359
1529
|
*/
|
|
1360
|
-
multiTenant?: boolean;
|
|
1361
|
-
/** Arc-compatible event transport. Default: in-process bus. */
|
|
1362
|
-
eventTransport?: EventTransport;
|
|
1530
|
+
multiTenant?: boolean | undefined;
|
|
1363
1531
|
/**
|
|
1364
|
-
*
|
|
1365
|
-
*
|
|
1366
|
-
*
|
|
1367
|
-
* rows. Without it, events publish post-write (or defer to a
|
|
1368
|
-
* `ctx[PENDING_EVENTS]` queue inside `engine.withTransaction`).
|
|
1532
|
+
* Progressive feature bundles (PACKAGE_RULES §22). Omit for the
|
|
1533
|
+
* all-enabled engine. When provided, unspecified modules are disabled;
|
|
1534
|
+
* dependencies are enabled automatically.
|
|
1369
1535
|
*/
|
|
1370
|
-
|
|
1371
|
-
/** Logger surface used by the in-process bus + repositories. */
|
|
1372
|
-
logger?: CrmLogger;
|
|
1373
|
-
/** Pagination defaults for every repository (mongokit). */
|
|
1374
|
-
pagination?: PaginationConfig;
|
|
1375
|
-
/** Extra mongokit plugins applied to every repository (after defaults). */
|
|
1376
|
-
plugins?: PluginType[];
|
|
1536
|
+
modules?: CrmModulesConfig | undefined;
|
|
1377
1537
|
/**
|
|
1378
1538
|
* On a Mongoose model-name collision (another engine / hot reload owns
|
|
1379
|
-
* the name): `false` (default) throws `
|
|
1380
|
-
*
|
|
1539
|
+
* the name): `false` (default) throws mongokit's `ModelCollisionError`;
|
|
1540
|
+
* `true` re-registers. Test fixtures / hot reload only (§21).
|
|
1381
1541
|
*/
|
|
1382
|
-
forceRecreate?: boolean;
|
|
1542
|
+
forceRecreate?: boolean | undefined;
|
|
1383
1543
|
/**
|
|
1384
1544
|
* Mongoose auto-index builds. Default `true` (dev). Set `false` in
|
|
1385
1545
|
* production and run `engine.syncIndexes()` from a deploy script so
|
|
1386
1546
|
* cold-boot index builds don't blow Fastify's plugin timeout (§35).
|
|
1387
1547
|
*/
|
|
1388
|
-
autoIndex?: boolean;
|
|
1548
|
+
autoIndex?: boolean | undefined;
|
|
1549
|
+
}
|
|
1550
|
+
/**
|
|
1551
|
+
* BIND-time runtime collaborators (Kernel Construction Standard §6.2). The live
|
|
1552
|
+
* connection is the first `bind` arg; these are the rest. A transport, outbox,
|
|
1553
|
+
* logger or plugin set belongs here, not in the shape.
|
|
1554
|
+
*/
|
|
1555
|
+
interface CrmRuntime {
|
|
1556
|
+
/** Arc-compatible event transport. Default: in-process bus (owned + closed by the engine). */
|
|
1557
|
+
eventTransport?: EventTransport | undefined;
|
|
1558
|
+
/**
|
|
1559
|
+
* Transactional outbox (PACKAGE_RULES §P8). When wired, every domain
|
|
1560
|
+
* event is persisted via `outbox.save(event, { session })` in the SAME
|
|
1561
|
+
* session as the business write — the host's relay publishes committed
|
|
1562
|
+
* rows. Without it, events publish post-write (or defer to a
|
|
1563
|
+
* `ctx[PENDING_EVENTS]` queue inside `engine.withTransaction`).
|
|
1564
|
+
*/
|
|
1565
|
+
outbox?: OutboxStore | undefined;
|
|
1566
|
+
/** Logger surface used by the in-process bus + repositories. */
|
|
1567
|
+
logger?: CrmLogger | undefined;
|
|
1568
|
+
/** Pagination defaults for every repository (mongokit). */
|
|
1569
|
+
pagination?: PaginationConfig | undefined;
|
|
1570
|
+
/** Extra mongokit plugins applied to every repository (after defaults). */
|
|
1571
|
+
plugins?: PluginType[] | undefined;
|
|
1389
1572
|
/**
|
|
1390
1573
|
* Skip the boot-time `transactions` capability requirement, downgrading
|
|
1391
1574
|
* it to a warning. Set only when the host never calls
|
|
1392
1575
|
* `LeadService.convert()` inside a transaction (standalone Mongo).
|
|
1393
1576
|
*/
|
|
1394
|
-
allowNonTransactional?: boolean;
|
|
1577
|
+
allowNonTransactional?: boolean | undefined;
|
|
1395
1578
|
}
|
|
1396
1579
|
interface ResolvedCrmConfig extends SchemaBuildConfig {
|
|
1397
1580
|
connection: Connection;
|
|
@@ -1399,8 +1582,9 @@ interface ResolvedCrmConfig extends SchemaBuildConfig {
|
|
|
1399
1582
|
multiTenant: boolean;
|
|
1400
1583
|
forceRecreate: boolean;
|
|
1401
1584
|
autoIndex: boolean;
|
|
1402
|
-
|
|
1403
|
-
|
|
1585
|
+
modules: ResolvedCrmModules;
|
|
1586
|
+
logger?: CrmLogger | undefined;
|
|
1587
|
+
pagination?: PaginationConfig | undefined;
|
|
1404
1588
|
plugins: PluginType[];
|
|
1405
1589
|
}
|
|
1406
1590
|
interface CrmRepositories {
|
|
@@ -1421,9 +1605,30 @@ interface CrmRepositories {
|
|
|
1421
1605
|
sequence: SequenceRepository;
|
|
1422
1606
|
enrollment: EnrollmentRepository;
|
|
1423
1607
|
}
|
|
1424
|
-
|
|
1608
|
+
type ModularCrmRepositories = Partial<CrmRepositories>;
|
|
1609
|
+
type SalesCrmModels = Pick<CrmModels, 'Pipeline' | 'Lead' | 'Opportunity' | 'Contact' | 'Account' | 'Activity' | 'Note' | 'LossReason'>;
|
|
1610
|
+
type SalesCrmRepositories = Pick<CrmRepositories, 'pipeline' | 'lead' | 'opportunity' | 'contact' | 'account' | 'activity' | 'note' | 'lossReason'>;
|
|
1611
|
+
type EngagementCrmModels = Pick<CrmModels, 'Subscription' | 'Suppression' | 'EmailEvent' | 'Conversation' | 'Message'>;
|
|
1612
|
+
type EngagementCrmRepositories = Pick<CrmRepositories, 'subscription' | 'suppression' | 'emailEvent' | 'conversation' | 'message'>;
|
|
1613
|
+
type AutomationCrmModels = Pick<CrmModels, 'Campaign' | 'Sequence' | 'Enrollment'>;
|
|
1614
|
+
type AutomationCrmRepositories = Pick<CrmRepositories, 'campaign' | 'sequence' | 'enrollment'>;
|
|
1615
|
+
interface CrmModuleView<TModels, TRepositories> {
|
|
1616
|
+
readonly models: Readonly<TModels>;
|
|
1617
|
+
readonly repositories: Readonly<TRepositories>;
|
|
1618
|
+
}
|
|
1619
|
+
interface ModularCrmModuleViews {
|
|
1620
|
+
readonly sales?: CrmModuleView<SalesCrmModels, SalesCrmRepositories> | undefined;
|
|
1621
|
+
readonly engagement?: CrmModuleView<EngagementCrmModels, EngagementCrmRepositories> | undefined;
|
|
1622
|
+
readonly automation?: CrmModuleView<AutomationCrmModels, AutomationCrmRepositories> | undefined;
|
|
1623
|
+
}
|
|
1624
|
+
interface FullCrmModuleViews {
|
|
1625
|
+
readonly sales: CrmModuleView<SalesCrmModels, SalesCrmRepositories>;
|
|
1626
|
+
readonly engagement: CrmModuleView<EngagementCrmModels, EngagementCrmRepositories>;
|
|
1627
|
+
readonly automation: CrmModuleView<AutomationCrmModels, AutomationCrmRepositories>;
|
|
1628
|
+
}
|
|
1629
|
+
interface CrmEngineBase<TModels, TRepositories, TModules> {
|
|
1425
1630
|
/** Repositories ARE the API surface (PACKAGE_RULES §1). */
|
|
1426
|
-
readonly repositories: Readonly<
|
|
1631
|
+
readonly repositories: Readonly<TRepositories>;
|
|
1427
1632
|
/**
|
|
1428
1633
|
* Arc-compatible event transport (a `CrmEventDispatcher` wrapping the
|
|
1429
1634
|
* configured transport — `publish`/`subscribe` behave exactly like the
|
|
@@ -1431,7 +1636,9 @@ interface CrmEngine {
|
|
|
1431
1636
|
*/
|
|
1432
1637
|
readonly events: CrmEventDispatcher;
|
|
1433
1638
|
/** Mongoose models — for arc adapter wiring. */
|
|
1434
|
-
readonly models: Readonly<
|
|
1639
|
+
readonly models: Readonly<TModels>;
|
|
1640
|
+
/** Capability-oriented views for hosts that opt into progressive modules. */
|
|
1641
|
+
readonly modules: Readonly<TModules>;
|
|
1435
1642
|
/** Resolved configuration (frozen). */
|
|
1436
1643
|
readonly config: Readonly<ResolvedCrmConfig>;
|
|
1437
1644
|
/**
|
|
@@ -1453,8 +1660,34 @@ interface CrmEngine {
|
|
|
1453
1660
|
* deploy when booted with `autoIndex: false` (§35). Idempotent.
|
|
1454
1661
|
*/
|
|
1455
1662
|
syncIndexes(): Promise<void>;
|
|
1456
|
-
/**
|
|
1457
|
-
|
|
1663
|
+
/**
|
|
1664
|
+
* Tear the engine down (Kernel Construction Standard §6.4 — the sole teardown
|
|
1665
|
+
* name). Closes the event bus ONLY when the engine created it itself
|
|
1666
|
+
* (`InProcessCrmBus`); a host-injected `eventTransport` is the host's to
|
|
1667
|
+
* close. Idempotent — a second call is a no-op. Mongoose
|
|
1668
|
+
* models/connection are untouched.
|
|
1669
|
+
*/
|
|
1670
|
+
close(): Promise<void>;
|
|
1671
|
+
}
|
|
1672
|
+
/** All-enabled engine returned when `shape.modules` is omitted. */
|
|
1673
|
+
type CrmEngine = CrmEngineBase<CrmModels, CrmRepositories, FullCrmModuleViews>;
|
|
1674
|
+
/** Progressive engine returned when a `modules` selection is supplied. */
|
|
1675
|
+
type ModularCrmEngine = CrmEngineBase<ModularCrmModels, ModularCrmRepositories, ModularCrmModuleViews>;
|
|
1676
|
+
/**
|
|
1677
|
+
* The connection-free description produced by `defineCrm(shape)` (Kernel
|
|
1678
|
+
* Construction Standard §6.1). `defineCrm` is the SOLE construction API — there
|
|
1679
|
+
* is no `createCrm` wrapper. `bind` compiles the schemas on the supplied
|
|
1680
|
+
* connection, wires the engine, verifies capabilities, and returns the engine.
|
|
1681
|
+
*/
|
|
1682
|
+
interface CrmBlueprint<TEngine extends CrmEngine | ModularCrmEngine = ModularCrmEngine> {
|
|
1683
|
+
readonly id: 'crm';
|
|
1684
|
+
/** The model names this blueprint registers — known without a connection. */
|
|
1685
|
+
readonly modelNames: readonly string[];
|
|
1686
|
+
/**
|
|
1687
|
+
* Compile + register the models on `connection`, wire repositories/transport,
|
|
1688
|
+
* verify capabilities, and return the frozen engine.
|
|
1689
|
+
*/
|
|
1690
|
+
bind(connection: Connection, runtime?: CrmRuntime): TEngine;
|
|
1458
1691
|
}
|
|
1459
1692
|
/**
|
|
1460
1693
|
* Operation context threaded through every domain verb. Extends
|
|
@@ -1463,22 +1696,28 @@ interface CrmEngine {
|
|
|
1463
1696
|
* `metadata?` field — keep the typed surface tight.
|
|
1464
1697
|
*/
|
|
1465
1698
|
interface CrmContext extends OperationContext {
|
|
1466
|
-
organizationId?: string |
|
|
1467
|
-
actorId?: string |
|
|
1699
|
+
organizationId?: string | import('mongoose').Types.ObjectId;
|
|
1700
|
+
actorId?: string | import('mongoose').Types.ObjectId;
|
|
1468
1701
|
/** Mongo transaction session (passed through to mongokit). */
|
|
1469
1702
|
session?: unknown;
|
|
1470
1703
|
}
|
|
1471
1704
|
type _ModelTypes = Model<unknown>;
|
|
1472
1705
|
//#endregion
|
|
1473
|
-
//#region src/engine/
|
|
1474
|
-
declare function
|
|
1706
|
+
//#region src/engine/define-crm.d.ts
|
|
1707
|
+
declare function defineCrm(shape: CrmShape & {
|
|
1708
|
+
modules: CrmModulesConfig;
|
|
1709
|
+
}): CrmBlueprint<ModularCrmEngine>;
|
|
1710
|
+
declare function defineCrm(shape?: CrmShape & {
|
|
1711
|
+
modules?: undefined;
|
|
1712
|
+
}): CrmBlueprint<CrmEngine>;
|
|
1713
|
+
declare function defineCrm(shape?: CrmShape): CrmBlueprint<ModularCrmEngine>;
|
|
1475
1714
|
//#endregion
|
|
1476
1715
|
//#region src/repositories/create-repositories.d.ts
|
|
1477
1716
|
interface CreateCrmRepositoriesDeps {
|
|
1478
1717
|
events: CrmEventDispatcher;
|
|
1479
1718
|
config: ResolvedCrmConfig;
|
|
1480
1719
|
}
|
|
1481
|
-
declare function createCrmRepositories(models:
|
|
1720
|
+
declare function createCrmRepositories(models: ModularCrmModels, deps: CreateCrmRepositoriesDeps): ModularCrmRepositories;
|
|
1482
1721
|
//#endregion
|
|
1483
1722
|
//#region src/services/lead.service.d.ts
|
|
1484
1723
|
interface ConvertLeadInput {
|
|
@@ -1598,9 +1837,11 @@ declare const CRM_EVENTS: {
|
|
|
1598
1837
|
readonly EMAIL_EVENT_UNSUBSCRIBED: "crm:email_event.unsubscribed";
|
|
1599
1838
|
readonly LEAD_ENRICHED: "crm:lead.enriched";
|
|
1600
1839
|
readonly LEAD_ASSIGNED: "crm:lead.assigned";
|
|
1840
|
+
readonly LEAD_ANONYMIZED: "crm:lead.anonymized";
|
|
1601
1841
|
readonly CONTACT_ENRICHED: "crm:contact.enriched";
|
|
1602
1842
|
readonly CONTACT_ASSIGNED: "crm:contact.assigned";
|
|
1603
1843
|
readonly CONTACT_MERGED: "crm:contact.merged";
|
|
1844
|
+
readonly CONTACT_ANONYMIZED: "crm:contact.anonymized";
|
|
1604
1845
|
readonly ACCOUNT_ENRICHED: "crm:account.enriched";
|
|
1605
1846
|
readonly ACCOUNT_ASSIGNED: "crm:account.assigned";
|
|
1606
1847
|
readonly ACCOUNT_MERGED: "crm:account.merged";
|
|
@@ -1666,6 +1907,10 @@ declare const LeadAssigned: CrmEventDefinition<z.ZodObject<{
|
|
|
1666
1907
|
ownerId: z.ZodString;
|
|
1667
1908
|
previousOwnerId: z.ZodOptional<z.ZodString>;
|
|
1668
1909
|
}, z.core.$strip>>;
|
|
1910
|
+
declare const LeadAnonymized: CrmEventDefinition<z.ZodObject<{
|
|
1911
|
+
leadId: z.ZodString;
|
|
1912
|
+
fields: z.ZodArray<z.ZodString>;
|
|
1913
|
+
}, z.core.$strip>>;
|
|
1669
1914
|
declare const AccountCreated: CrmEventDefinition<z.ZodObject<{
|
|
1670
1915
|
accountId: z.ZodString;
|
|
1671
1916
|
name: z.ZodString;
|
|
@@ -1716,6 +1961,10 @@ declare const ContactMerged: CrmEventDefinition<z.ZodObject<{
|
|
|
1716
1961
|
preferSecondary: "preferSecondary";
|
|
1717
1962
|
}>;
|
|
1718
1963
|
}, z.core.$strip>>;
|
|
1964
|
+
declare const ContactAnonymized: CrmEventDefinition<z.ZodObject<{
|
|
1965
|
+
contactId: z.ZodString;
|
|
1966
|
+
fields: z.ZodArray<z.ZodString>;
|
|
1967
|
+
}, z.core.$strip>>;
|
|
1719
1968
|
declare const OpportunityCreated: CrmEventDefinition<z.ZodObject<{
|
|
1720
1969
|
opportunityId: z.ZodString;
|
|
1721
1970
|
pipelineId: z.ZodString;
|
|
@@ -1984,16 +2233,10 @@ declare const crmEventDefinitions: ReadonlyArray<CrmEventDefinition>;
|
|
|
1984
2233
|
declare function createEvent<T>(type: string, payload: T, ctx?: CrmContext, meta?: Partial<EventMeta>): DomainEvent<T>;
|
|
1985
2234
|
//#endregion
|
|
1986
2235
|
//#region src/events/in-process-bus.d.ts
|
|
1987
|
-
declare class InProcessCrmBus
|
|
1988
|
-
readonly name = "in-process-crm";
|
|
1989
|
-
private subs;
|
|
1990
|
-
private logger?;
|
|
2236
|
+
declare class InProcessCrmBus extends InProcessEventBus {
|
|
1991
2237
|
constructor(options?: {
|
|
1992
2238
|
logger?: CrmLogger;
|
|
1993
2239
|
});
|
|
1994
|
-
publish(event: DomainEvent): Promise<void>;
|
|
1995
|
-
subscribe(pattern: string, handler: EventHandler): Promise<() => void>;
|
|
1996
|
-
close(): Promise<void>;
|
|
1997
2240
|
}
|
|
1998
2241
|
//#endregion
|
|
1999
2242
|
//#region src/engine/run-unit.d.ts
|
|
@@ -2023,27 +2266,40 @@ declare function runUnit<T>(config: RunUnitConfig, ctx: CrmContext, body: (inner
|
|
|
2023
2266
|
* Built on `@classytic/primitives/state-machine` so the same shape lights
|
|
2024
2267
|
* up across kits (mongokit's `claim()` pairing, sqlitekit follow-ups).
|
|
2025
2268
|
*/
|
|
2026
|
-
declare const LEAD_MACHINE:
|
|
2027
|
-
declare const OPPORTUNITY_MACHINE:
|
|
2028
|
-
declare const CAMPAIGN_MACHINE:
|
|
2029
|
-
declare const SUBSCRIPTION_MACHINE:
|
|
2030
|
-
declare const ACTIVITY_MACHINE:
|
|
2031
|
-
declare const SEQUENCE_MACHINE:
|
|
2032
|
-
declare const ENROLLMENT_MACHINE:
|
|
2269
|
+
declare const LEAD_MACHINE: import("@classytic/primitives/state-machine").StateMachine<"new" | "contacted" | "qualified" | "converted" | "disqualified" | "nurturing">;
|
|
2270
|
+
declare const OPPORTUNITY_MACHINE: import("@classytic/primitives/state-machine").StateMachine<"open" | "won" | "lost" | "abandoned">;
|
|
2271
|
+
declare const CAMPAIGN_MACHINE: import("@classytic/primitives/state-machine").StateMachine<"draft" | "active" | "paused" | "completed" | "archived">;
|
|
2272
|
+
declare const SUBSCRIPTION_MACHINE: import("@classytic/primitives/state-machine").StateMachine<"pending" | "confirmed" | "unsubscribed" | "bounced" | "complained">;
|
|
2273
|
+
declare const ACTIVITY_MACHINE: import("@classytic/primitives/state-machine").StateMachine<"completed" | "planned" | "in_progress" | "cancelled">;
|
|
2274
|
+
declare const SEQUENCE_MACHINE: import("@classytic/primitives/state-machine").StateMachine<"draft" | "active" | "paused" | "archived">;
|
|
2275
|
+
declare const ENROLLMENT_MACHINE: import("@classytic/primitives/state-machine").StateMachine<"active" | "paused" | "completed" | "exited">;
|
|
2033
2276
|
//#endregion
|
|
2034
2277
|
//#region src/core/errors.d.ts
|
|
2035
2278
|
/**
|
|
2036
|
-
*
|
|
2279
|
+
* `status` makes every CRM throwable satisfy `HttpError` from
|
|
2280
|
+
* `@classytic/repo-core/errors` (PACKAGE_RULES "Errors and wire envelopes"),
|
|
2281
|
+
* so arc's global handler serializes it natively with the right code AND the
|
|
2282
|
+
* right status — no per-host `errorMapper`. Without it every CRM error
|
|
2283
|
+
* reached the client as `arc.internal_error` / 500: `NotFoundError` answered
|
|
2284
|
+
* 500 instead of 404, which tells a caller "the server broke" when the truth
|
|
2285
|
+
* was "that lead doesn't exist".
|
|
2286
|
+
*
|
|
2287
|
+
* Defaults to 422 (well-formed request, violates a domain invariant) —
|
|
2288
|
+
* the common case here. Subclasses override with `readonly status = N`;
|
|
2289
|
+
* the subclass field initializer runs after this constructor, so it wins.
|
|
2037
2290
|
*/
|
|
2038
|
-
declare class CrmError extends Error {
|
|
2291
|
+
declare class CrmError extends Error implements HttpError {
|
|
2039
2292
|
readonly code: string;
|
|
2293
|
+
readonly status: number;
|
|
2040
2294
|
readonly details?: Record<string, unknown>;
|
|
2041
2295
|
constructor(code: string, message: string, details?: Record<string, unknown>);
|
|
2042
2296
|
}
|
|
2043
2297
|
declare class NotFoundError extends CrmError {
|
|
2298
|
+
readonly status = 404;
|
|
2044
2299
|
constructor(kind: string, id: string);
|
|
2045
2300
|
}
|
|
2046
2301
|
declare class InvalidLeadTransitionError extends CrmError {
|
|
2302
|
+
readonly status = 422;
|
|
2047
2303
|
constructor(details: {
|
|
2048
2304
|
from: string;
|
|
2049
2305
|
to: string;
|
|
@@ -2051,6 +2307,7 @@ declare class InvalidLeadTransitionError extends CrmError {
|
|
|
2051
2307
|
});
|
|
2052
2308
|
}
|
|
2053
2309
|
declare class InvalidOpportunityTransitionError extends CrmError {
|
|
2310
|
+
readonly status = 422;
|
|
2054
2311
|
constructor(details: {
|
|
2055
2312
|
from: string;
|
|
2056
2313
|
to: string;
|
|
@@ -2058,6 +2315,7 @@ declare class InvalidOpportunityTransitionError extends CrmError {
|
|
|
2058
2315
|
});
|
|
2059
2316
|
}
|
|
2060
2317
|
declare class InvalidActivityTransitionError extends CrmError {
|
|
2318
|
+
readonly status = 422;
|
|
2061
2319
|
constructor(details: {
|
|
2062
2320
|
from: string;
|
|
2063
2321
|
to: string;
|
|
@@ -2065,27 +2323,39 @@ declare class InvalidActivityTransitionError extends CrmError {
|
|
|
2065
2323
|
});
|
|
2066
2324
|
}
|
|
2067
2325
|
declare class StageNotInPipelineError extends CrmError {
|
|
2326
|
+
readonly status = 422;
|
|
2068
2327
|
constructor(details: {
|
|
2069
2328
|
pipelineId: string;
|
|
2070
2329
|
stageId: string;
|
|
2071
2330
|
});
|
|
2072
2331
|
}
|
|
2073
2332
|
declare class LeadAlreadyConvertedError extends CrmError {
|
|
2333
|
+
readonly status = 409;
|
|
2074
2334
|
constructor(leadId: string);
|
|
2075
2335
|
}
|
|
2076
2336
|
declare class DuplicateLeadError extends CrmError {
|
|
2337
|
+
readonly status = 409;
|
|
2077
2338
|
constructor(details: {
|
|
2078
2339
|
email?: string;
|
|
2079
2340
|
phone?: string;
|
|
2080
2341
|
});
|
|
2081
2342
|
}
|
|
2343
|
+
declare class DuplicateContactError extends CrmError {
|
|
2344
|
+
readonly status = 409;
|
|
2345
|
+
constructor(details: {
|
|
2346
|
+
email: string;
|
|
2347
|
+
});
|
|
2348
|
+
}
|
|
2082
2349
|
declare class UnmanagedSessionError extends CrmError {
|
|
2350
|
+
readonly status = 500;
|
|
2083
2351
|
constructor(eventType?: string);
|
|
2084
2352
|
}
|
|
2085
2353
|
declare class EmptyPipelineError extends CrmError {
|
|
2354
|
+
readonly status = 422;
|
|
2086
2355
|
constructor(pipelineId: string);
|
|
2087
2356
|
}
|
|
2088
2357
|
declare class InvalidCampaignTransitionError extends CrmError {
|
|
2358
|
+
readonly status = 422;
|
|
2089
2359
|
constructor(details: {
|
|
2090
2360
|
from: string;
|
|
2091
2361
|
to: string;
|
|
@@ -2093,9 +2363,11 @@ declare class InvalidCampaignTransitionError extends CrmError {
|
|
|
2093
2363
|
});
|
|
2094
2364
|
}
|
|
2095
2365
|
declare class DuplicateCampaignError extends CrmError {
|
|
2366
|
+
readonly status = 409;
|
|
2096
2367
|
constructor(name: string);
|
|
2097
2368
|
}
|
|
2098
2369
|
declare class InvalidSubscriptionTransitionError extends CrmError {
|
|
2370
|
+
readonly status = 422;
|
|
2099
2371
|
constructor(details: {
|
|
2100
2372
|
from: string;
|
|
2101
2373
|
to: string;
|
|
@@ -2103,18 +2375,22 @@ declare class InvalidSubscriptionTransitionError extends CrmError {
|
|
|
2103
2375
|
});
|
|
2104
2376
|
}
|
|
2105
2377
|
declare class DuplicateSubscriptionError extends CrmError {
|
|
2378
|
+
readonly status = 409;
|
|
2106
2379
|
constructor(details: {
|
|
2107
2380
|
contactId: string;
|
|
2108
2381
|
campaignId: string;
|
|
2109
2382
|
});
|
|
2110
2383
|
}
|
|
2111
2384
|
declare class InvalidConfirmationTokenError extends CrmError {
|
|
2385
|
+
readonly status = 422;
|
|
2112
2386
|
constructor();
|
|
2113
2387
|
}
|
|
2114
2388
|
declare class InvalidUnsubscribeTokenError extends CrmError {
|
|
2389
|
+
readonly status = 422;
|
|
2115
2390
|
constructor();
|
|
2116
2391
|
}
|
|
2117
2392
|
declare class InvalidSequenceTransitionError extends CrmError {
|
|
2393
|
+
readonly status = 422;
|
|
2118
2394
|
constructor(details: {
|
|
2119
2395
|
from: string;
|
|
2120
2396
|
to: string;
|
|
@@ -2122,6 +2398,7 @@ declare class InvalidSequenceTransitionError extends CrmError {
|
|
|
2122
2398
|
});
|
|
2123
2399
|
}
|
|
2124
2400
|
declare class InvalidEnrollmentTransitionError extends CrmError {
|
|
2401
|
+
readonly status = 422;
|
|
2125
2402
|
constructor(details: {
|
|
2126
2403
|
from: string;
|
|
2127
2404
|
to: string;
|
|
@@ -2129,11 +2406,111 @@ declare class InvalidEnrollmentTransitionError extends CrmError {
|
|
|
2129
2406
|
});
|
|
2130
2407
|
}
|
|
2131
2408
|
declare class AlreadyEnrolledError extends CrmError {
|
|
2409
|
+
readonly status = 409;
|
|
2132
2410
|
constructor(details: {
|
|
2133
2411
|
sequenceId: string;
|
|
2134
2412
|
subjectId: string;
|
|
2135
2413
|
});
|
|
2136
2414
|
}
|
|
2415
|
+
/**
|
|
2416
|
+
* An address handed to a suppression-list WRITE could not be canonicalised.
|
|
2417
|
+
*
|
|
2418
|
+
* 400, not 422 — the request itself is malformed at the field, no domain
|
|
2419
|
+
* invariant was consulted. This is `@classytic/contact`'s `EmailFormatError`
|
|
2420
|
+
* re-thrown as a CRM error so arc serializes it natively; the same shape party
|
|
2421
|
+
* uses for `PartyPhoneFormatError`. `contact` sits below every domain kernel
|
|
2422
|
+
* and knows nothing about HTTP, so the mapping has to happen here.
|
|
2423
|
+
*
|
|
2424
|
+
* Only WRITES throw. Reads fail closed instead — see
|
|
2425
|
+
* `suppression.repository.ts`, which writes the asymmetry down.
|
|
2426
|
+
*/
|
|
2427
|
+
declare class SuppressionEmailFormatError extends CrmError {
|
|
2428
|
+
readonly status = 400;
|
|
2429
|
+
constructor(details: {
|
|
2430
|
+
email: string;
|
|
2431
|
+
reason: string;
|
|
2432
|
+
});
|
|
2433
|
+
}
|
|
2434
|
+
/**
|
|
2435
|
+
* An address handed to `LeadRepository.createLead` could not be canonicalised.
|
|
2436
|
+
*
|
|
2437
|
+
* 400, same shape and same reason as `SuppressionEmailFormatError`: the field is
|
|
2438
|
+
* malformed, no domain invariant was consulted. Thrown from
|
|
2439
|
+
* `core/email-identity.ts`, which owns the write-throws / setter-degrades
|
|
2440
|
+
* asymmetry and writes down why each side goes the way it does.
|
|
2441
|
+
*/
|
|
2442
|
+
declare class LeadEmailFormatError extends CrmError {
|
|
2443
|
+
readonly status = 400;
|
|
2444
|
+
constructor(details: {
|
|
2445
|
+
email: string;
|
|
2446
|
+
reason: string;
|
|
2447
|
+
});
|
|
2448
|
+
}
|
|
2449
|
+
/** As {@link LeadEmailFormatError}, for `ContactRepository.createContact`. */
|
|
2450
|
+
declare class ContactEmailFormatError extends CrmError {
|
|
2451
|
+
readonly status = 400;
|
|
2452
|
+
constructor(details: {
|
|
2453
|
+
email: string;
|
|
2454
|
+
reason: string;
|
|
2455
|
+
});
|
|
2456
|
+
}
|
|
2457
|
+
//#endregion
|
|
2458
|
+
//#region src/core/email-identity.d.ts
|
|
2459
|
+
/**
|
|
2460
|
+
* WHERE the email key rule is applied inside CRM. **Not a second copy of the
|
|
2461
|
+
* rule** — the rule is `normalizeEmail` from `@classytic/contact/email`, imported
|
|
2462
|
+
* directly here as it is in `suppression.repository.ts`. What this module owns is
|
|
2463
|
+
* the two things a shared kernel cannot decide for us: which mongoose fields carry
|
|
2464
|
+
* the key, and what happens when the kernel refuses an input.
|
|
2465
|
+
*
|
|
2466
|
+
* It exists as a module rather than a copy-paste because Lead and Contact both
|
|
2467
|
+
* carry an email identity. Two schemas hand-rolling the same setter is the drift
|
|
2468
|
+
* this package already paid for once.
|
|
2469
|
+
*
|
|
2470
|
+
* ## Why the SETTER, and not only the repository verbs
|
|
2471
|
+
*
|
|
2472
|
+
* `LeadRepository.createLead` / `ContactRepository.createContact` are NOT on the
|
|
2473
|
+
* HTTP path. `@spinekit/crm` mounts both resources through arc's mongoose CRUD
|
|
2474
|
+
* adapter, so `POST /crm/leads` calls the inherited `Repository.create()` and the
|
|
2475
|
+
* verbs' pre-check never runs. A fix that lived only on the verbs would leave the
|
|
2476
|
+
* live route forking one mailbox into two rows, and the tests for the verbs would
|
|
2477
|
+
* be green while it did.
|
|
2478
|
+
*
|
|
2479
|
+
* The narrowest boundary EVERY write must cross is the schema field, so the key
|
|
2480
|
+
* rule is applied there. That is also what makes the partial-unique
|
|
2481
|
+
* `(organizationId, email)` index authoritative: an index can only dedup what the
|
|
2482
|
+
* key already folded, and `lowercase: true` does not IDNA-fold. `ada@bücher.de`
|
|
2483
|
+
* and `ada@xn--bcher-kva.de` are one host in two encodings — the index saw two
|
|
2484
|
+
* strings and admitted both, no error, one mailbox as two leads.
|
|
2485
|
+
*
|
|
2486
|
+
* Mongoose runs string setters on QUERY FILTERS as well as on writes (this is why
|
|
2487
|
+
* `lowercase: true` already made `getOne({ email: 'ADA@x.com' })` match a stored
|
|
2488
|
+
* `ada@x.com`), so the setter fixes the read side in the same stroke. The
|
|
2489
|
+
* repository verbs still canonicalise EXPLICITLY before querying — see
|
|
2490
|
+
* `requireLeadEmailKey`'s call sites. That is not redundancy for its own sake:
|
|
2491
|
+
* a pre-check that depends on an invisible schema option silently WIDENS the day
|
|
2492
|
+
* someone edits the field, and a widened filter does not fail, it matches nothing
|
|
2493
|
+
* and reports "no duplicate".
|
|
2494
|
+
*
|
|
2495
|
+
* ## The setter must NEVER throw, and it does not
|
|
2496
|
+
*
|
|
2497
|
+
* A throw inside a setter surfaces during query casting as an opaque `CastError`,
|
|
2498
|
+
* which would turn a malformed filter into a 500 on a READ. So the setter degrades
|
|
2499
|
+
* to the LEGACY folding (`trim().toLowerCase()`) for anything the kernel refuses —
|
|
2500
|
+
* exactly the behaviour those values have today, so nothing already stored is
|
|
2501
|
+
* re-keyed by a rule that cannot explain itself.
|
|
2502
|
+
*
|
|
2503
|
+
* Refusing is the WRITE VERBS' job, where a typed 400 can be thrown and the caller
|
|
2504
|
+
* is a deliberate domain call rather than a filter. Same asymmetry, same reason as
|
|
2505
|
+
* `suppression.repository.ts`: the safe direction differs per side.
|
|
2506
|
+
*/
|
|
2507
|
+
/**
|
|
2508
|
+
* The canonical key for `email`, or `null` when it has none.
|
|
2509
|
+
*
|
|
2510
|
+
* Exported so a host can tell "not on file" apart from "not an address" without
|
|
2511
|
+
* re-deriving the rule — the same reason `suppressionEmailKey` is exported.
|
|
2512
|
+
*/
|
|
2513
|
+
declare function crmEmailKey(email: string): string | null;
|
|
2137
2514
|
//#endregion
|
|
2138
2515
|
//#region src/shared/calculators/forecast.d.ts
|
|
2139
2516
|
type OpportunityForecast = Pick<IOpportunityDocument, 'amount' | 'currency' | 'probability' | 'status' | 'expectedCloseAt' | 'stageId' | 'statusHistory'>;
|
|
@@ -2826,4 +3203,4 @@ declare namespace index_d_exports {
|
|
|
2826
3203
|
export { AddNoteDto, CancelActivityDto, CloseLossDto, CloseWinDto, CompleteCampaignDto, ConfirmSubscriptionDto, ConvertLeadDto, CreateAccountDto, CreateCampaignDto, CreateContactDto, CreateLeadDto, CreateLossReasonDto, CreatePipelineDto, CreateSequenceInput, LeadFilterDto, MoveStageDto, OpenOpportunityDto, OpportunityFilterDto, PlanActivityDto, RecordEmailEventDto, SequenceFilterInput, StageInputDto, SubscribeDto, SuppressDto, UnsubscribeDto, UnsuppressDto, UpdateAccountDto, UpdateCampaignDto, UpdateContactDto, UpdateLeadDto, UpdateLossReasonDto, UpdateNoteDto, UpdatePipelineDto, UpdateSequenceInput, accountFilterSchema, activityFilterSchema, addNoteSchema, campaignFilterSchema, cancelActivitySchema, closeLossSchema, closeWinSchema, completeCampaignSchema, confirmSubscriptionSchema, contactFilterSchema, convertLeadSchema, createAccountSchema, createCampaignSchema, createContactSchema, createLeadSchema, createLossReasonSchema, createPipelineSchema, createSequenceSchema, currencyCodeSchema, emailEventFilterSchema, leadFilterSchema, lossReasonFilterSchema, metadataSchema, moneyAmountSchema, moveStageSchema, noteFilterSchema, objectIdLike, openOpportunitySchema, opportunityFilterSchema, pipelineFilterSchema, planActivitySchema, recordEmailEventSchema, sequenceFilterSchema, sequenceStepSchema, stageInputSchemaExport, subscribeSchema, subscriptionFilterSchema, suppressSchema, suppressionFilterSchema, tagsSchema, unsubscribeSchema, unsuppressSchema, updateAccountSchema, updateActivitySchema, updateCampaignSchema, updateContactSchema, updateLeadSchema, updateLossReasonSchema, updateNoteSchema, updatePipelineSchema, updateSequenceSchema };
|
|
2827
3204
|
}
|
|
2828
3205
|
//#endregion
|
|
2829
|
-
export { ACTIVITY_MACHINE, ACTIVITY_STATUSES, ACTIVITY_TRANSITIONS, ACTIVITY_TYPES, AccountAssigned, AccountCreated, AccountEnriched, AccountMerged, AccountRepository, AccountRepositoryDeps, AccountUpdated, ActivityCancelled, ActivityCompleted, ActivityOutcomeRecorded, ActivityPlanned, ActivityRepository, ActivityRepositoryDeps, ActivityRescheduled, type ActivityStatus, type ActivityType, AlreadyEnrolledError, CAMPAIGN_MACHINE, CAMPAIGN_STATUSES, CAMPAIGN_TRANSITIONS, CONVERSATION_CHANNELS, CONVERSATION_PARTICIPANT_KINDS, CONVERSATION_STATUSES, CRM_EVENTS, CRM_MODEL_NAMES, CampaignActivated, CampaignArchived, CampaignCompleted, CampaignCreated, CampaignPaused, CampaignRepository, CampaignRepositoryDeps, CampaignResumed, type CampaignStatus, CloseInput, ContactAssigned, ContactCreated, ContactEnriched, ContactMerged, ContactRepository, ContactRepositoryDeps, ContactUpdated, ConversationAssigned, type ConversationChannel, ConversationCreated, type ConversationParticipantKind, ConversationReopened, ConversationRepository, ConversationRepositoryDeps, ConversationResolved, type ConversationStatus, ConversionRow, ConvertLeadInput, ConvertResult, CreateCrmRepositoriesDeps,
|
|
3206
|
+
export { ACTIVITY_MACHINE, ACTIVITY_STATUSES, ACTIVITY_TRANSITIONS, ACTIVITY_TYPES, ALL_CRM_MODULES, AccountAssigned, AccountCreated, AccountEnriched, AccountMerged, AccountRepository, AccountRepositoryDeps, AccountUpdated, ActivityCancelled, ActivityCompleted, ActivityOutcomeRecorded, ActivityPlanned, ActivityRepository, ActivityRepositoryDeps, ActivityRescheduled, type ActivityStatus, type ActivityType, AlreadyEnrolledError, AutomationCrmModels, AutomationCrmRepositories, CAMPAIGN_MACHINE, CAMPAIGN_STATUSES, CAMPAIGN_TRANSITIONS, CONVERSATION_CHANNELS, CONVERSATION_PARTICIPANT_KINDS, CONVERSATION_STATUSES, CRM_EVENTS, CRM_MODEL_NAMES, CampaignActivated, CampaignArchived, CampaignCompleted, CampaignCreated, CampaignPaused, CampaignRepository, CampaignRepositoryDeps, CampaignResumed, type CampaignStatus, CloseInput, ContactAnonymized, ContactAssigned, ContactCreated, ContactEmailFormatError, ContactEnriched, ContactMergeRepoint, ContactMerged, ContactRepository, ContactRepositoryDeps, ContactUpdated, ConversationAssigned, type ConversationChannel, ConversationCreated, type ConversationParticipantKind, ConversationReopened, ConversationRepository, ConversationRepositoryDeps, ConversationResolved, type ConversationStatus, ConversionRow, ConvertLeadInput, ConvertResult, CreateCrmRepositoriesDeps, CrmBlueprint, CrmContext, CrmEngine, CrmEngineBase, CrmError, CrmEventDefinition, CrmEventDispatcher, type CrmEventDispatcherOptions, CrmEventName, CrmEventSchema, CrmLogger, CrmModels, CrmModuleView, CrmModulesConfig, CrmRepositories, CrmRuntime, CrmShape, DefineCrmModelsConfig, DuplicateCampaignError, DuplicateContactError, DuplicateLeadError, DuplicateSubscriptionError, EMAIL_EVENT_KINDS, EMAIL_EVENT_SUBJECT_KINDS, ENROLLMENT_MACHINE, ENROLLMENT_STATUSES, ENROLLMENT_SUBJECT_KINDS, ENROLLMENT_TRANSITIONS, EVENT_DERIVED_SUPPRESSION_REASONS, EmailEventBounced, EmailEventClicked, EmailEventComplained, type EmailEventKind, EmailEventOpened, EmailEventRepository, EmailEventRepositoryDeps, EmailEventSubjectKind, EmailEventUnsubscribed, EmptyPipelineError, EngagementCrmModels, EngagementCrmRepositories, EnrollInput, EnrollmentCompleted, EnrollmentCreated, EnrollmentExitReason, EnrollmentExited, EnrollmentPaused, EnrollmentRepository, EnrollmentRepositoryDeps, EnrollmentResumed, type EnrollmentStatus, EnrollmentStepSent, EnrollmentSubjectKind, ForecastOptions, ForecastRow, FullCrmModuleViews, IAccountDocument, IActivityDocument, ICampaignDocument, ICampaignStatusEntry, IContactDocument, IContactInfoEmbedded, IContactName, IConversationDocument, IDelayStep, IEmailEventDocument, IEnrollmentDocument, IEnrollmentStatusEntry, ILeadDocument, ILeadEngagement, ILeadStatusEntry, ILossReasonDocument, IMessageAttachment, IMessageDocument, INoteDocument, IOpportunityDocument, IOpportunityStatusEntry, IPipelineDocument, ISendEmailStep, ISequenceDocument, ISequenceStatusEntry, IStageDocument, IStepResult, ISubscriptionDocument, ISubscriptionStatusEntry, ISuppressionDocument, InProcessCrmBus, type InternalCrmContext, InvalidActivityTransitionError, InvalidCampaignTransitionError, InvalidConfirmationTokenError, InvalidEnrollmentTransitionError, InvalidLeadTransitionError, InvalidOpportunityTransitionError, InvalidSequenceTransitionError, InvalidSubscriptionTransitionError, InvalidUnsubscribeTokenError, LEAD_MACHINE, LEAD_STATUSES, LEAD_TRANSITIONS, LeadAlreadyConvertedError, LeadAnonymized, LeadAssigned, LeadConverted, LeadCreated, LeadDisqualified, LeadEmailFormatError, LeadEngagementSignal, LeadEnriched, LeadInput, LeadRepository, LeadRepositoryDeps, LeadScored, LeadScorer, LeadService, LeadServiceDeps, type LeadStatus, LeadStatusChanged, LossReasonRepository, LossReasonRepositoryDeps, MESSAGE_DELIVERY_STATUSES, MESSAGE_DIRECTIONS, MergeAccountInput, MergeContactInput, type MessageDeliveryStatus, type MessageDirection, MessageReceived, MessageRepository, MessageRepositoryDeps, MessageSent, ModularCrmEngine, ModularCrmModels, ModularCrmModuleViews, ModularCrmRepositories, MoveStageInput, NotFoundError, NoteAdded, NoteRepository, NoteRepositoryDeps, OPPORTUNITY_MACHINE, OPPORTUNITY_STATUSES, OPPORTUNITY_TRANSITIONS, OpenConversationInput, OpenOpportunityInput, OpportunityAbandoned, OpportunityAmountChanged, OpportunityAssigned, OpportunityCreated, OpportunityExtended, OpportunityForecast, OpportunityLost, OpportunityRepository, OpportunityRepositoryDeps, OpportunityStageAdvanced, OpportunityStageRegressed, type OpportunityStatus, OpportunityWon, PENDING_EVENTS, PipelineArchived, PipelineCreated, PipelineRepository, PipelineRepositoryDeps, RecordEmailEventInput, RecordMessageInput, ReplySubject, ResolvedCrmConfig, ResolvedCrmModules, type RunUnitConfig, SEQUENCE_MACHINE, SEQUENCE_STATUSES, SEQUENCE_TRANSITIONS, SUBJECT_KINDS, SUBSCRIPTION_MACHINE, SUBSCRIPTION_STATUSES, SUBSCRIPTION_TRANSITIONS, SUPPRESSION_REASONS, SalesCrmModels, SalesCrmRepositories, SchemaBuildConfig, SequenceActivated, SequenceArchived, SequenceCreated, SequencePaused, SequenceRepository, SequenceRepositoryDeps, type SequenceStatus, SequenceStep, StageNotInPipelineError, type StatusChangeEntry, type StatusHistory, StepResultStatus, type SubjectKind, SubscribeInput, SubscriptionBounced, SubscriptionComplained, SubscriptionConfirmed, SubscriptionCreated, SubscriptionRepository, SubscriptionRepositoryDeps, type SubscriptionStatus, SubscriptionUnsubscribed, SuppressInput, SuppressionAdded, SuppressionEmailFormatError, type SuppressionReason, SuppressionRemoved, SuppressionRepository, SuppressionRepositoryDeps, TenantFieldType, UnmanagedSessionError, _ModelTypes, appendStatus, buildAccountSchema, buildActivitySchema, buildCampaignSchema, buildContactSchema, buildConversationSchema, buildEmailEventSchema, buildEnrollmentSchema, buildLeadSchema, buildLossReasonSchema, buildMessageSchema, buildNoteSchema, buildOpportunitySchema, buildPipelineSchema, buildSequenceSchema, buildSubscriptionSchema, buildSuppressionSchema, canTransitionActivity, canTransitionCampaign, canTransitionEnrollment, canTransitionLead, canTransitionOpportunity, canTransitionSequence, canTransitionSubscription, composeScorers, createAccountModel, createActivityModel, createCampaignModel, createContactModel, createConversationModel, createCrmRepositories, createEmailEventModel, createEvent, createLeadModel, createLossReasonModel, createMessageModel, createNoteModel, createOpportunityModel, createPipelineModel, createSubscriptionModel, crmEmailKey, crmEventDefinitions, crmModelName, defaultLeadScorer, defineCrm, defineCrmModels, flushPending, forecast, lastTransitionTo, latestEntry, pipelineConversion, resolveCrmModules, runUnit, suppressionEmailKey, tenantFieldDef, timeInStatus, index_d_exports as validators };
|