@classytic/crm 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/dist/index.d.ts +36 -9
- package/dist/index.js +58 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@classytic/crm` are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.8.0] — 2026-07-11
|
|
6
|
+
|
|
7
|
+
### Added — engagement-based lead scoring
|
|
8
|
+
- **`ILeadEngagement` on the Lead** (`{ opens, clicks, replies, activities, lastEngagedAt? }`, nested default `{}` so old rows read as zeros). Behavioural counters, distinct from firmographic fields.
|
|
9
|
+
- **`LeadRepository.recordEngagement(id, signal, ctx)`** where `signal ∈ 'open'|'click'|'reply'|'activity'` (`LeadEngagementSignal`). Atomic `$inc` on the matching counter + `lastEngagedAt` via `findOneAndUpdate`, then `recomputeScore`. Hosts feed it off `crm:message.received` (reply), `crm:activity.*` (activity), and email open/click tracking.
|
|
10
|
+
- **`defaultLeadScorer` now weighs engagement** on top of firmographic fit: reply ×25 · click ×8 · open ×2 · activity ×5, capped at +80. `LeadInput` picks up `engagement`. Pure/deterministic — no wall-clock. Absent engagement scores as zero (score unchanged for pre-existing leads until a signal lands).
|
|
11
|
+
|
|
5
12
|
## [0.7.0] — 2026-07-05
|
|
6
13
|
|
|
7
14
|
### Added — org-wide email suppression list
|
package/dist/index.d.ts
CHANGED
|
@@ -124,6 +124,19 @@ interface ILeadStatusEntry {
|
|
|
124
124
|
by?: string;
|
|
125
125
|
note?: string;
|
|
126
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Behavioural engagement counters — fed by the host from CRM events
|
|
129
|
+
* (email opens/clicks, inbound replies, logged activities) via
|
|
130
|
+
* `LeadRepository.recordEngagement`. Read by the lead scorer alongside
|
|
131
|
+
* the firmographic fields. Zero-valued on old rows (nested default).
|
|
132
|
+
*/
|
|
133
|
+
interface ILeadEngagement {
|
|
134
|
+
opens: number;
|
|
135
|
+
clicks: number;
|
|
136
|
+
replies: number;
|
|
137
|
+
activities: number;
|
|
138
|
+
lastEngagedAt?: Date;
|
|
139
|
+
}
|
|
127
140
|
interface ILeadDocument {
|
|
128
141
|
_id: _$mongoose.Types.ObjectId;
|
|
129
142
|
organizationId: _$mongoose.Types.ObjectId | string;
|
|
@@ -137,6 +150,7 @@ interface ILeadDocument {
|
|
|
137
150
|
source?: string;
|
|
138
151
|
campaignRef?: string;
|
|
139
152
|
score: number;
|
|
153
|
+
engagement: ILeadEngagement;
|
|
140
154
|
status: LeadStatus;
|
|
141
155
|
statusHistory: ILeadStatusEntry[];
|
|
142
156
|
ownerId?: string;
|
|
@@ -751,24 +765,29 @@ declare class PipelineRepository extends Repository<IPipelineDocument> {
|
|
|
751
765
|
}
|
|
752
766
|
//#endregion
|
|
753
767
|
//#region src/shared/calculators/scoring.d.ts
|
|
754
|
-
/** Minimal Lead shape the scorer reads. */
|
|
755
|
-
type LeadInput = Pick<ILeadDocument, 'email' | 'phone' | 'companyName' | 'tags' | 'jobTitle'>;
|
|
768
|
+
/** Minimal Lead shape the scorer reads — firmographic + engagement counters. */
|
|
769
|
+
type LeadInput = Pick<ILeadDocument, 'email' | 'phone' | 'companyName' | 'tags' | 'jobTitle' | 'engagement'>;
|
|
756
770
|
type LeadScorer = (lead: LeadInput) => number;
|
|
757
771
|
/**
|
|
758
772
|
* Reference implementation. Intentionally simple — hosts read it and
|
|
759
|
-
* replace it.
|
|
773
|
+
* replace it. Deterministic + pure (no I/O, no wall-clock).
|
|
760
774
|
*
|
|
761
|
-
*
|
|
762
|
-
* +20
|
|
763
|
-
*
|
|
764
|
-
*
|
|
765
|
-
*
|
|
775
|
+
* Firmographic fit (max 110):
|
|
776
|
+
* +20 email · +20 phone · +20 companyName
|
|
777
|
+
* +10 per populated tag, up to +40
|
|
778
|
+
* +10 if jobTitle hints senior (director/manager/head/chief/vp/founder)
|
|
779
|
+
*
|
|
780
|
+
* Behavioural engagement, capped at +80 so a run of opens can't dominate a
|
|
781
|
+
* genuine reply: reply ×25 · click ×8 · open ×2 · activity ×5. Counters are
|
|
782
|
+
* fed by the host via `LeadRepository.recordEngagement`; absent on old rows.
|
|
766
783
|
*/
|
|
767
784
|
declare function defaultLeadScorer(lead: LeadInput): number;
|
|
768
785
|
/** Compose multiple small rules into one scorer. */
|
|
769
786
|
declare function composeScorers(...rules: readonly LeadScorer[]): LeadScorer;
|
|
770
787
|
//#endregion
|
|
771
788
|
//#region src/repositories/lead.repository.d.ts
|
|
789
|
+
/** Behavioural engagement signals fed via `recordEngagement`. */
|
|
790
|
+
type LeadEngagementSignal = 'open' | 'click' | 'reply' | 'activity';
|
|
772
791
|
interface LeadRepositoryDeps {
|
|
773
792
|
events: CrmEventDispatcher;
|
|
774
793
|
/** Override the default lead scorer. */
|
|
@@ -813,6 +832,14 @@ declare class LeadRepository extends Repository<ILeadDocument> {
|
|
|
813
832
|
* the previous owner so audit consumers can compute hand-off chains.
|
|
814
833
|
*/
|
|
815
834
|
assign(id: string, ownerId: string, ctx?: CrmContext): Promise<ILeadDocument>;
|
|
835
|
+
/**
|
|
836
|
+
* Record a behavioural engagement signal, then re-score. The host feeds
|
|
837
|
+
* these off CRM events (`crm:message.received` → reply, `crm:activity.*` →
|
|
838
|
+
* activity, and email open/click tracking → open/click). Atomic `$inc`
|
|
839
|
+
* on the counter (single round-trip, no lost-update window under
|
|
840
|
+
* concurrent signals), then `recomputeScore` re-reads the fresh counters.
|
|
841
|
+
*/
|
|
842
|
+
recordEngagement(id: string, signal: LeadEngagementSignal, ctx?: CrmContext): Promise<ILeadDocument>;
|
|
816
843
|
/**
|
|
817
844
|
* Recompute + persist the lead's score. Called from `createLead`
|
|
818
845
|
* automatically; hosts also call this after enrichment writes.
|
|
@@ -2799,4 +2826,4 @@ declare namespace index_d_exports {
|
|
|
2799
2826
|
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 };
|
|
2800
2827
|
}
|
|
2801
2828
|
//#endregion
|
|
2802
|
-
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, CreateModelsOptions, CrmConfig, CrmContext, CrmEngine, CrmError, CrmEventDefinition, CrmEventDispatcher, type CrmEventDispatcherOptions, CrmEventName, CrmEventSchema, CrmLogger, CrmModels, CrmRepositories, DuplicateCampaignError, DuplicateLeadError, DuplicateSubscriptionError, EMAIL_EVENT_KINDS, EMAIL_EVENT_SUBJECT_KINDS, ENROLLMENT_MACHINE, ENROLLMENT_STATUSES, ENROLLMENT_SUBJECT_KINDS, ENROLLMENT_TRANSITIONS, EVENT_DERIVED_SUPPRESSION_REASONS, EmailEventBounced, EmailEventClicked, EmailEventComplained, type EmailEventKind, EmailEventOpened, EmailEventRepository, EmailEventRepositoryDeps, EmailEventSubjectKind, EmailEventUnsubscribed, EmptyPipelineError, EnrollInput, EnrollmentCompleted, EnrollmentCreated, EnrollmentExitReason, EnrollmentExited, EnrollmentPaused, EnrollmentRepository, EnrollmentRepositoryDeps, EnrollmentResumed, type EnrollmentStatus, EnrollmentStepSent, EnrollmentSubjectKind, ForecastOptions, ForecastRow, type IAccountDocument, type IActivityDocument, type ICampaignDocument, ICampaignStatusEntry, type IContactDocument, IContactInfoEmbedded, IContactName, type IConversationDocument, IDelayStep, type IEmailEventDocument, type IEnrollmentDocument, IEnrollmentStatusEntry, type ILeadDocument, ILeadStatusEntry, type ILossReasonDocument, IMessageAttachment, type IMessageDocument, type INoteDocument, type IOpportunityDocument, IOpportunityStatusEntry, type IPipelineDocument, ISendEmailStep, type ISequenceDocument, ISequenceStatusEntry, IStageDocument, IStepResult, type ISubscriptionDocument, ISubscriptionStatusEntry, type ISuppressionDocument, InProcessCrmBus, type InternalCrmContext, InvalidActivityTransitionError, InvalidCampaignTransitionError, InvalidConfirmationTokenError, InvalidEnrollmentTransitionError, InvalidLeadTransitionError, InvalidOpportunityTransitionError, InvalidSequenceTransitionError, InvalidSubscriptionTransitionError, InvalidUnsubscribeTokenError, LEAD_MACHINE, LEAD_STATUSES, LEAD_TRANSITIONS, LeadAlreadyConvertedError, LeadAssigned, LeadConverted, LeadCreated, LeadDisqualified, 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, 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, type RunUnitConfig, SEQUENCE_MACHINE, SEQUENCE_STATUSES, SEQUENCE_TRANSITIONS, SUBJECT_KINDS, SUBSCRIPTION_MACHINE, SUBSCRIPTION_STATUSES, SUBSCRIPTION_TRANSITIONS, SUPPRESSION_REASONS, 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, 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, createCrm, createCrmModels, createCrmRepositories, createEmailEventModel, createEvent, createLeadModel, createLossReasonModel, createMessageModel, createNoteModel, createOpportunityModel, createPipelineModel, createSubscriptionModel, crmEventDefinitions, defaultLeadScorer, flushPending, forecast, lastTransitionTo, latestEntry, pipelineConversion, runUnit, tenantFieldDef, timeInStatus, index_d_exports as validators };
|
|
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, CreateModelsOptions, CrmConfig, CrmContext, CrmEngine, CrmError, CrmEventDefinition, CrmEventDispatcher, type CrmEventDispatcherOptions, CrmEventName, CrmEventSchema, CrmLogger, CrmModels, CrmRepositories, DuplicateCampaignError, DuplicateLeadError, DuplicateSubscriptionError, EMAIL_EVENT_KINDS, EMAIL_EVENT_SUBJECT_KINDS, ENROLLMENT_MACHINE, ENROLLMENT_STATUSES, ENROLLMENT_SUBJECT_KINDS, ENROLLMENT_TRANSITIONS, EVENT_DERIVED_SUPPRESSION_REASONS, EmailEventBounced, EmailEventClicked, EmailEventComplained, type EmailEventKind, EmailEventOpened, EmailEventRepository, EmailEventRepositoryDeps, EmailEventSubjectKind, EmailEventUnsubscribed, EmptyPipelineError, EnrollInput, EnrollmentCompleted, EnrollmentCreated, EnrollmentExitReason, EnrollmentExited, EnrollmentPaused, EnrollmentRepository, EnrollmentRepositoryDeps, EnrollmentResumed, type EnrollmentStatus, EnrollmentStepSent, EnrollmentSubjectKind, ForecastOptions, ForecastRow, type IAccountDocument, type IActivityDocument, type ICampaignDocument, ICampaignStatusEntry, type IContactDocument, IContactInfoEmbedded, IContactName, type IConversationDocument, IDelayStep, type IEmailEventDocument, type IEnrollmentDocument, IEnrollmentStatusEntry, type ILeadDocument, ILeadEngagement, ILeadStatusEntry, type ILossReasonDocument, IMessageAttachment, type IMessageDocument, type INoteDocument, type IOpportunityDocument, IOpportunityStatusEntry, type IPipelineDocument, ISendEmailStep, type ISequenceDocument, ISequenceStatusEntry, IStageDocument, IStepResult, type ISubscriptionDocument, ISubscriptionStatusEntry, type ISuppressionDocument, InProcessCrmBus, type InternalCrmContext, InvalidActivityTransitionError, InvalidCampaignTransitionError, InvalidConfirmationTokenError, InvalidEnrollmentTransitionError, InvalidLeadTransitionError, InvalidOpportunityTransitionError, InvalidSequenceTransitionError, InvalidSubscriptionTransitionError, InvalidUnsubscribeTokenError, LEAD_MACHINE, LEAD_STATUSES, LEAD_TRANSITIONS, LeadAlreadyConvertedError, LeadAssigned, LeadConverted, LeadCreated, LeadDisqualified, 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, 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, type RunUnitConfig, SEQUENCE_MACHINE, SEQUENCE_STATUSES, SEQUENCE_TRANSITIONS, SUBJECT_KINDS, SUBSCRIPTION_MACHINE, SUBSCRIPTION_STATUSES, SUBSCRIPTION_TRANSITIONS, SUPPRESSION_REASONS, 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, 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, createCrm, createCrmModels, createCrmRepositories, createEmailEventModel, createEvent, createLeadModel, createLossReasonModel, createMessageModel, createNoteModel, createOpportunityModel, createPipelineModel, createSubscriptionModel, crmEventDefinitions, defaultLeadScorer, flushPending, forecast, lastTransitionTo, latestEntry, pipelineConversion, runUnit, tenantFieldDef, timeInStatus, index_d_exports as validators };
|
package/dist/index.js
CHANGED
|
@@ -292,6 +292,25 @@ const statusEntrySchema$5 = new Schema({
|
|
|
292
292
|
by: { type: String },
|
|
293
293
|
note: { type: String }
|
|
294
294
|
}, { _id: false });
|
|
295
|
+
const engagementSchema = new Schema({
|
|
296
|
+
opens: {
|
|
297
|
+
type: Number,
|
|
298
|
+
default: 0
|
|
299
|
+
},
|
|
300
|
+
clicks: {
|
|
301
|
+
type: Number,
|
|
302
|
+
default: 0
|
|
303
|
+
},
|
|
304
|
+
replies: {
|
|
305
|
+
type: Number,
|
|
306
|
+
default: 0
|
|
307
|
+
},
|
|
308
|
+
activities: {
|
|
309
|
+
type: Number,
|
|
310
|
+
default: 0
|
|
311
|
+
},
|
|
312
|
+
lastEngagedAt: { type: Date }
|
|
313
|
+
}, { _id: false });
|
|
295
314
|
function buildLeadSchema(config) {
|
|
296
315
|
const schema = new Schema({
|
|
297
316
|
organizationId: tenantFieldDef(config),
|
|
@@ -346,6 +365,10 @@ function buildLeadSchema(config) {
|
|
|
346
365
|
type: Number,
|
|
347
366
|
default: 0
|
|
348
367
|
},
|
|
368
|
+
engagement: {
|
|
369
|
+
type: engagementSchema,
|
|
370
|
+
default: () => ({})
|
|
371
|
+
},
|
|
349
372
|
status: {
|
|
350
373
|
type: String,
|
|
351
374
|
enum: LEAD_STATUSES,
|
|
@@ -2240,13 +2263,16 @@ const ENROLLMENT_MACHINE = defineStateMachine({
|
|
|
2240
2263
|
//#region src/shared/calculators/scoring.ts
|
|
2241
2264
|
/**
|
|
2242
2265
|
* Reference implementation. Intentionally simple — hosts read it and
|
|
2243
|
-
* replace it.
|
|
2266
|
+
* replace it. Deterministic + pure (no I/O, no wall-clock).
|
|
2267
|
+
*
|
|
2268
|
+
* Firmographic fit (max 110):
|
|
2269
|
+
* +20 email · +20 phone · +20 companyName
|
|
2270
|
+
* +10 per populated tag, up to +40
|
|
2271
|
+
* +10 if jobTitle hints senior (director/manager/head/chief/vp/founder)
|
|
2244
2272
|
*
|
|
2245
|
-
* +
|
|
2246
|
-
*
|
|
2247
|
-
*
|
|
2248
|
-
* +10 per populated tag up to 40
|
|
2249
|
-
* +10 if jobTitle hints senior (director / manager / head / chief / vp / founder)
|
|
2273
|
+
* Behavioural engagement, capped at +80 so a run of opens can't dominate a
|
|
2274
|
+
* genuine reply: reply ×25 · click ×8 · open ×2 · activity ×5. Counters are
|
|
2275
|
+
* fed by the host via `LeadRepository.recordEngagement`; absent on old rows.
|
|
2250
2276
|
*/
|
|
2251
2277
|
function defaultLeadScorer(lead) {
|
|
2252
2278
|
let score = 0;
|
|
@@ -2255,6 +2281,11 @@ function defaultLeadScorer(lead) {
|
|
|
2255
2281
|
if (lead.companyName) score += 20;
|
|
2256
2282
|
score += Math.min(40, (lead.tags?.length ?? 0) * 10);
|
|
2257
2283
|
if (lead.jobTitle && /(director|manager|head|chief|vp|founder)/i.test(lead.jobTitle)) score += 10;
|
|
2284
|
+
const e = lead.engagement;
|
|
2285
|
+
if (e) {
|
|
2286
|
+
const engagementPoints = (e.replies ?? 0) * 25 + (e.clicks ?? 0) * 8 + (e.opens ?? 0) * 2 + (e.activities ?? 0) * 5;
|
|
2287
|
+
score += Math.min(80, engagementPoints);
|
|
2288
|
+
}
|
|
2258
2289
|
return score;
|
|
2259
2290
|
}
|
|
2260
2291
|
/** Compose multiple small rules into one scorer. */
|
|
@@ -2263,6 +2294,13 @@ function composeScorers(...rules) {
|
|
|
2263
2294
|
}
|
|
2264
2295
|
//#endregion
|
|
2265
2296
|
//#region src/repositories/lead.repository.ts
|
|
2297
|
+
/** signal → engagement counter field. */
|
|
2298
|
+
const ENGAGEMENT_COUNTER = {
|
|
2299
|
+
open: "opens",
|
|
2300
|
+
click: "clicks",
|
|
2301
|
+
reply: "replies",
|
|
2302
|
+
activity: "activities"
|
|
2303
|
+
};
|
|
2266
2304
|
var LeadRepository = class extends Repository {
|
|
2267
2305
|
events;
|
|
2268
2306
|
scorer;
|
|
@@ -2383,6 +2421,20 @@ var LeadRepository = class extends Repository {
|
|
|
2383
2421
|
return await this.getById(id, ctx) ?? before;
|
|
2384
2422
|
}
|
|
2385
2423
|
/**
|
|
2424
|
+
* Record a behavioural engagement signal, then re-score. The host feeds
|
|
2425
|
+
* these off CRM events (`crm:message.received` → reply, `crm:activity.*` →
|
|
2426
|
+
* activity, and email open/click tracking → open/click). Atomic `$inc`
|
|
2427
|
+
* on the counter (single round-trip, no lost-update window under
|
|
2428
|
+
* concurrent signals), then `recomputeScore` re-reads the fresh counters.
|
|
2429
|
+
*/
|
|
2430
|
+
async recordEngagement(id, signal, ctx) {
|
|
2431
|
+
if (!await this.findOneAndUpdate({ _id: id }, {
|
|
2432
|
+
$inc: { [`engagement.${ENGAGEMENT_COUNTER[signal]}`]: 1 },
|
|
2433
|
+
$set: { "engagement.lastEngagedAt": /* @__PURE__ */ new Date() }
|
|
2434
|
+
}, ctx)) throw new NotFoundError("Lead", id);
|
|
2435
|
+
return this.recomputeScore(id, ctx);
|
|
2436
|
+
}
|
|
2437
|
+
/**
|
|
2386
2438
|
* Recompute + persist the lead's score. Called from `createLead`
|
|
2387
2439
|
* automatically; hosts also call this after enrichment writes.
|
|
2388
2440
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classytic/crm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Engine-factory CRM package — leads, accounts, contacts, opportunities, pipelines, campaigns, subscriptions, activities, notes, email engagement. PACKAGE_RULES compliant; built on Mongoose + mongokit + arc primitives.",
|
|
5
5
|
"author": "Classytic",
|
|
6
6
|
"license": "MIT",
|