@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 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
- * +20 if email is present
762
- * +20 if phone is present
763
- * +20 if companyName is present
764
- * +10 per populated tag up to 40
765
- * +10 if jobTitle hints senior (director / manager / head / chief / vp / founder)
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
- * +20 if email is present
2246
- * +20 if phone is present
2247
- * +20 if companyName is present
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.7.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",