@classytic/crm 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
- import { t as __exportAll } from "./rolldown-runtime.js";
2
- import { AccountAssigned, AccountCreated, AccountEnriched, AccountMerged, AccountUpdated, ActivityCancelled, ActivityCompleted, ActivityOutcomeRecorded, ActivityPlanned, ActivityRescheduled, CRM_EVENTS, CampaignActivated, CampaignArchived, CampaignCompleted, CampaignCreated, CampaignPaused, CampaignResumed, ContactAssigned, ContactCreated, ContactEnriched, ContactMerged, ContactUpdated, ConversationAssigned, ConversationCreated, ConversationReopened, ConversationResolved, EmailEventBounced, EmailEventClicked, EmailEventComplained, EmailEventOpened, EmailEventUnsubscribed, InProcessCrmBus, LeadAssigned, LeadConverted, LeadCreated, LeadDisqualified, LeadEnriched, LeadScored, LeadStatusChanged, MessageReceived, MessageSent, NoteAdded, OpportunityAbandoned, OpportunityAmountChanged, OpportunityAssigned, OpportunityCreated, OpportunityExtended, OpportunityLost, OpportunityStageAdvanced, OpportunityStageRegressed, OpportunityWon, PipelineArchived, PipelineCreated, SubscriptionBounced, SubscriptionComplained, SubscriptionConfirmed, SubscriptionCreated, SubscriptionUnsubscribed, createEvent, crmEventDefinitions } from "./events.js";
1
+ import { t as __exportAll } from "./chunk.js";
3
2
  import { Schema } from "mongoose";
4
3
  import { Repository, aggregateHelpersPlugin, isDuplicateKeyError, methodRegistryPlugin, multiTenantPlugin } from "@classytic/mongokit";
4
+ import { createEvent as createEvent$1, matchEventPattern } from "@classytic/primitives/events";
5
5
  import { assertAndClaim, defineStateMachine } from "@classytic/primitives/state-machine";
6
6
  import { randomUUID } from "node:crypto";
7
7
  import { z } from "zod";
@@ -1653,6 +1653,86 @@ function createCrmModels(connection, config) {
1653
1653
  });
1654
1654
  }
1655
1655
  //#endregion
1656
+ //#region src/events/event-constants.ts
1657
+ /** PACKAGE_RULES rule 12: `package:resource.verb`. */
1658
+ const CRM_EVENTS = {
1659
+ LEAD_CREATED: "crm:lead.created",
1660
+ LEAD_STATUS_CHANGED: "crm:lead.status_changed",
1661
+ LEAD_SCORED: "crm:lead.scored",
1662
+ LEAD_CONVERTED: "crm:lead.converted",
1663
+ LEAD_DISQUALIFIED: "crm:lead.disqualified",
1664
+ ACCOUNT_CREATED: "crm:account.created",
1665
+ ACCOUNT_UPDATED: "crm:account.updated",
1666
+ CONTACT_CREATED: "crm:contact.created",
1667
+ CONTACT_UPDATED: "crm:contact.updated",
1668
+ OPPORTUNITY_CREATED: "crm:opportunity.created",
1669
+ OPPORTUNITY_STAGE_ADVANCED: "crm:opportunity.stage_advanced",
1670
+ OPPORTUNITY_STAGE_REGRESSED: "crm:opportunity.stage_regressed",
1671
+ OPPORTUNITY_WON: "crm:opportunity.won",
1672
+ OPPORTUNITY_LOST: "crm:opportunity.lost",
1673
+ OPPORTUNITY_ABANDONED: "crm:opportunity.abandoned",
1674
+ PIPELINE_CREATED: "crm:pipeline.created",
1675
+ PIPELINE_ARCHIVED: "crm:pipeline.archived",
1676
+ ACTIVITY_PLANNED: "crm:activity.planned",
1677
+ ACTIVITY_COMPLETED: "crm:activity.completed",
1678
+ ACTIVITY_CANCELLED: "crm:activity.cancelled",
1679
+ NOTE_ADDED: "crm:note.added",
1680
+ CAMPAIGN_CREATED: "crm:campaign.created",
1681
+ CAMPAIGN_ACTIVATED: "crm:campaign.activated",
1682
+ CAMPAIGN_PAUSED: "crm:campaign.paused",
1683
+ CAMPAIGN_RESUMED: "crm:campaign.resumed",
1684
+ CAMPAIGN_COMPLETED: "crm:campaign.completed",
1685
+ CAMPAIGN_ARCHIVED: "crm:campaign.archived",
1686
+ SUBSCRIPTION_CREATED: "crm:subscription.created",
1687
+ SUBSCRIPTION_CONFIRMED: "crm:subscription.confirmed",
1688
+ SUBSCRIPTION_UNSUBSCRIBED: "crm:subscription.unsubscribed",
1689
+ SUBSCRIPTION_BOUNCED: "crm:subscription.bounced",
1690
+ SUBSCRIPTION_COMPLAINED: "crm:subscription.complained",
1691
+ EMAIL_EVENT_OPENED: "crm:email_event.opened",
1692
+ EMAIL_EVENT_CLICKED: "crm:email_event.clicked",
1693
+ EMAIL_EVENT_BOUNCED: "crm:email_event.bounced",
1694
+ EMAIL_EVENT_COMPLAINED: "crm:email_event.complained",
1695
+ EMAIL_EVENT_UNSUBSCRIBED: "crm:email_event.unsubscribed",
1696
+ LEAD_ENRICHED: "crm:lead.enriched",
1697
+ LEAD_ASSIGNED: "crm:lead.assigned",
1698
+ CONTACT_ENRICHED: "crm:contact.enriched",
1699
+ CONTACT_ASSIGNED: "crm:contact.assigned",
1700
+ CONTACT_MERGED: "crm:contact.merged",
1701
+ ACCOUNT_ENRICHED: "crm:account.enriched",
1702
+ ACCOUNT_ASSIGNED: "crm:account.assigned",
1703
+ ACCOUNT_MERGED: "crm:account.merged",
1704
+ OPPORTUNITY_ASSIGNED: "crm:opportunity.assigned",
1705
+ OPPORTUNITY_AMOUNT_CHANGED: "crm:opportunity.amount_changed",
1706
+ OPPORTUNITY_EXTENDED: "crm:opportunity.extended",
1707
+ ACTIVITY_RESCHEDULED: "crm:activity.rescheduled",
1708
+ ACTIVITY_OUTCOME_RECORDED: "crm:activity.outcome_recorded",
1709
+ CONVERSATION_CREATED: "crm:conversation.created",
1710
+ CONVERSATION_ASSIGNED: "crm:conversation.assigned",
1711
+ CONVERSATION_RESOLVED: "crm:conversation.resolved",
1712
+ CONVERSATION_REOPENED: "crm:conversation.reopened",
1713
+ MESSAGE_RECEIVED: "crm:message.received",
1714
+ MESSAGE_SENT: "crm:message.sent"
1715
+ };
1716
+ //#endregion
1717
+ //#region src/events/helpers.ts
1718
+ /**
1719
+ * Event creation helper.
1720
+ *
1721
+ * Thin wrapper around `@classytic/primitives`' `createEvent` that fills the
1722
+ * shared `EventMeta` from a `CrmContext` (userId/actorId, organizationId,
1723
+ * correlationId). Use this instead of importing `createEvent` directly so
1724
+ * the `source: 'crm'` stamp + tenant scoping stay consistent.
1725
+ */
1726
+ function createEvent(type, payload, ctx, meta) {
1727
+ return createEvent$1(type, payload, {
1728
+ source: "crm",
1729
+ ...ctx?.actorId !== void 0 ? { actorId: String(ctx.actorId) } : {},
1730
+ ...ctx?.organizationId !== void 0 ? { organizationId: String(ctx.organizationId) } : {},
1731
+ ...ctx?.correlationId !== void 0 ? { correlationId: ctx.correlationId } : {},
1732
+ ...meta
1733
+ });
1734
+ }
1735
+ //#endregion
1656
1736
  //#region src/repositories/pipeline.repository.ts
1657
1737
  var PipelineRepository = class extends Repository {
1658
1738
  events;
@@ -3170,6 +3250,43 @@ function createCrmRepositories(models, deps) {
3170
3250
  });
3171
3251
  }
3172
3252
  //#endregion
3253
+ //#region src/events/in-process-bus.ts
3254
+ /**
3255
+ * In-process event bus — default fallback when no `eventTransport` is
3256
+ * provided to `createCrm`. Structurally identical to arc's
3257
+ * `MemoryEventTransport`; pattern matching is delegated to primitives.
3258
+ * Handler errors are swallowed — event emission never blocks writes.
3259
+ */
3260
+ var InProcessCrmBus = class {
3261
+ name = "in-process-crm";
3262
+ subs = [];
3263
+ logger;
3264
+ constructor(options) {
3265
+ if (options?.logger) this.logger = options.logger;
3266
+ }
3267
+ async publish(event) {
3268
+ const matching = this.subs.filter((s) => matchEventPattern(s.pattern, event.type));
3269
+ for (const sub of matching) try {
3270
+ await sub.handler(event);
3271
+ } catch (err) {
3272
+ this.logger?.error?.(`[crm] event handler error [${event.type}]`, { error: err instanceof Error ? err.message : String(err) });
3273
+ }
3274
+ }
3275
+ async subscribe(pattern, handler) {
3276
+ const sub = {
3277
+ pattern,
3278
+ handler
3279
+ };
3280
+ this.subs.push(sub);
3281
+ return () => {
3282
+ this.subs = this.subs.filter((s) => s !== sub);
3283
+ };
3284
+ }
3285
+ async close() {
3286
+ this.subs = [];
3287
+ }
3288
+ };
3289
+ //#endregion
3173
3290
  //#region src/engine/ensure-ready.ts
3174
3291
  const REQUIRED_CAPABILITIES = [
3175
3292
  "upsert",
@@ -3400,6 +3517,602 @@ function splitLeadName(lead) {
3400
3517
  };
3401
3518
  }
3402
3519
  //#endregion
3520
+ //#region src/events/event-catalog.ts
3521
+ /**
3522
+ * Zod-source event catalog (PACKAGE_RULES rule 18).
3523
+ *
3524
+ * One Zod schema per event is the source of truth — runtime validation,
3525
+ * static types, and Arc introspection all derive from it. Hosts wire the
3526
+ * definitions into arc's `EventRegistry`:
3527
+ *
3528
+ * ```ts
3529
+ * for (const def of crmEventDefinitions) registry.register(def);
3530
+ * ```
3531
+ *
3532
+ * which lights up publish-time validation (`eventPlugin({ registry,
3533
+ * validateMode: 'reject' })`), OpenAPI event introspection, and
3534
+ * `arc describe` output — with zero arc imports here (the definition shape
3535
+ * is structurally identical to arc's `EventDefinitionOutput`).
3536
+ *
3537
+ * Timestamps in payloads are `z.iso.datetime()` (NEVER `z.coerce.date()`):
3538
+ * `z.toJSONSchema()` throws on coerce.date, and events are ISO strings on
3539
+ * the wire (rule 25).
3540
+ */
3541
+ function defineCrmEvent(input) {
3542
+ const { name, resource, description, version = 1, zodSchema } = input;
3543
+ return {
3544
+ name,
3545
+ version,
3546
+ description,
3547
+ zodSchema,
3548
+ schema: z.toJSONSchema(zodSchema),
3549
+ create(payload, meta) {
3550
+ return createEvent$1(name, payload, {
3551
+ resource,
3552
+ source: "crm",
3553
+ ...meta
3554
+ });
3555
+ }
3556
+ };
3557
+ }
3558
+ const ownerChangeSchema = z.object({
3559
+ ownerId: z.string(),
3560
+ previousOwnerId: z.string().optional()
3561
+ });
3562
+ const enrichSchema = z.object({
3563
+ fields: z.array(z.string()),
3564
+ provider: z.string().optional()
3565
+ });
3566
+ const mergeSchema = z.object({
3567
+ primaryId: z.string(),
3568
+ secondaryId: z.string(),
3569
+ strategy: z.enum(["preferPrimary", "preferSecondary"])
3570
+ });
3571
+ const stageRefSchema = z.object({
3572
+ stageId: z.string(),
3573
+ sequence: z.number()
3574
+ });
3575
+ const LeadCreated = defineCrmEvent({
3576
+ name: CRM_EVENTS.LEAD_CREATED,
3577
+ resource: "lead",
3578
+ description: "createLead() inserted a scored lead.",
3579
+ zodSchema: z.object({
3580
+ leadId: z.string(),
3581
+ email: z.string().optional(),
3582
+ score: z.number()
3583
+ })
3584
+ });
3585
+ const LeadStatusChanged = defineCrmEvent({
3586
+ name: CRM_EVENTS.LEAD_STATUS_CHANGED,
3587
+ resource: "lead",
3588
+ description: "A lead lifecycle transition committed (qualify/disqualify/nurture/...).",
3589
+ zodSchema: z.object({
3590
+ leadId: z.string(),
3591
+ from: z.string(),
3592
+ to: z.string()
3593
+ })
3594
+ });
3595
+ const LeadScored = defineCrmEvent({
3596
+ name: CRM_EVENTS.LEAD_SCORED,
3597
+ resource: "lead",
3598
+ description: "A lead score was (re)computed and persisted.",
3599
+ zodSchema: z.object({
3600
+ leadId: z.string(),
3601
+ score: z.number()
3602
+ })
3603
+ });
3604
+ const LeadConverted = defineCrmEvent({
3605
+ name: CRM_EVENTS.LEAD_CONVERTED,
3606
+ resource: "lead",
3607
+ description: "convert() spawned Contact (+ Account) + Opportunity and closed the lead.",
3608
+ zodSchema: z.object({
3609
+ leadId: z.string(),
3610
+ contactId: z.string(),
3611
+ accountId: z.string().optional(),
3612
+ opportunityId: z.string()
3613
+ })
3614
+ });
3615
+ const LeadDisqualified = defineCrmEvent({
3616
+ name: CRM_EVENTS.LEAD_DISQUALIFIED,
3617
+ resource: "lead",
3618
+ description: "disqualify() rejected a lead with a reason.",
3619
+ zodSchema: z.object({
3620
+ leadId: z.string(),
3621
+ reason: z.string()
3622
+ })
3623
+ });
3624
+ const LeadEnriched = defineCrmEvent({
3625
+ name: CRM_EVENTS.LEAD_ENRICHED,
3626
+ resource: "lead",
3627
+ description: "enrich() merged a provider/manual patch into the lead.",
3628
+ zodSchema: z.object({ leadId: z.string() }).extend(enrichSchema.shape)
3629
+ });
3630
+ const LeadAssigned = defineCrmEvent({
3631
+ name: CRM_EVENTS.LEAD_ASSIGNED,
3632
+ resource: "lead",
3633
+ description: "assign() (re)assigned the lead owner.",
3634
+ zodSchema: z.object({ leadId: z.string() }).extend(ownerChangeSchema.shape)
3635
+ });
3636
+ const AccountCreated = defineCrmEvent({
3637
+ name: CRM_EVENTS.ACCOUNT_CREATED,
3638
+ resource: "account",
3639
+ description: "createAccount() inserted an account.",
3640
+ zodSchema: z.object({
3641
+ accountId: z.string(),
3642
+ name: z.string()
3643
+ })
3644
+ });
3645
+ const AccountUpdated = defineCrmEvent({
3646
+ name: CRM_EVENTS.ACCOUNT_UPDATED,
3647
+ resource: "account",
3648
+ description: "An account record was updated.",
3649
+ zodSchema: z.object({ accountId: z.string() })
3650
+ });
3651
+ const AccountEnriched = defineCrmEvent({
3652
+ name: CRM_EVENTS.ACCOUNT_ENRICHED,
3653
+ resource: "account",
3654
+ description: "enrich() merged a patch into the account.",
3655
+ zodSchema: z.object({ accountId: z.string() }).extend(enrichSchema.shape)
3656
+ });
3657
+ const AccountAssigned = defineCrmEvent({
3658
+ name: CRM_EVENTS.ACCOUNT_ASSIGNED,
3659
+ resource: "account",
3660
+ description: "assign() (re)assigned the account owner.",
3661
+ zodSchema: z.object({ accountId: z.string() }).extend(ownerChangeSchema.shape)
3662
+ });
3663
+ const AccountMerged = defineCrmEvent({
3664
+ name: CRM_EVENTS.ACCOUNT_MERGED,
3665
+ resource: "account",
3666
+ description: "merge() folded a secondary account into the primary; secondary deleted.",
3667
+ zodSchema: mergeSchema
3668
+ });
3669
+ const ContactCreated = defineCrmEvent({
3670
+ name: CRM_EVENTS.CONTACT_CREATED,
3671
+ resource: "contact",
3672
+ description: "createContact() inserted a contact.",
3673
+ zodSchema: z.object({
3674
+ contactId: z.string(),
3675
+ email: z.string().optional()
3676
+ })
3677
+ });
3678
+ const ContactUpdated = defineCrmEvent({
3679
+ name: CRM_EVENTS.CONTACT_UPDATED,
3680
+ resource: "contact",
3681
+ description: "A contact record was updated.",
3682
+ zodSchema: z.object({ contactId: z.string() })
3683
+ });
3684
+ const ContactEnriched = defineCrmEvent({
3685
+ name: CRM_EVENTS.CONTACT_ENRICHED,
3686
+ resource: "contact",
3687
+ description: "enrich() merged a patch into the contact.",
3688
+ zodSchema: z.object({ contactId: z.string() }).extend(enrichSchema.shape)
3689
+ });
3690
+ const ContactAssigned = defineCrmEvent({
3691
+ name: CRM_EVENTS.CONTACT_ASSIGNED,
3692
+ resource: "contact",
3693
+ description: "assign() (re)assigned the contact owner.",
3694
+ zodSchema: z.object({ contactId: z.string() }).extend(ownerChangeSchema.shape)
3695
+ });
3696
+ const ContactMerged = defineCrmEvent({
3697
+ name: CRM_EVENTS.CONTACT_MERGED,
3698
+ resource: "contact",
3699
+ description: "merge() folded a secondary contact into the primary; secondary deleted.",
3700
+ zodSchema: mergeSchema
3701
+ });
3702
+ const OpportunityCreated = defineCrmEvent({
3703
+ name: CRM_EVENTS.OPPORTUNITY_CREATED,
3704
+ resource: "opportunity",
3705
+ description: "openOpportunity() created a deal in a pipeline stage.",
3706
+ zodSchema: z.object({
3707
+ opportunityId: z.string(),
3708
+ pipelineId: z.string(),
3709
+ stageId: z.string()
3710
+ })
3711
+ });
3712
+ const OpportunityStageAdvanced = defineCrmEvent({
3713
+ name: CRM_EVENTS.OPPORTUNITY_STAGE_ADVANCED,
3714
+ resource: "opportunity",
3715
+ description: "moveToStage() moved the deal forward (higher stage sequence).",
3716
+ zodSchema: z.object({
3717
+ opportunityId: z.string(),
3718
+ from: stageRefSchema,
3719
+ to: stageRefSchema
3720
+ })
3721
+ });
3722
+ const OpportunityStageRegressed = defineCrmEvent({
3723
+ name: CRM_EVENTS.OPPORTUNITY_STAGE_REGRESSED,
3724
+ resource: "opportunity",
3725
+ description: "moveToStage() moved the deal backward (lower stage sequence).",
3726
+ zodSchema: z.object({
3727
+ opportunityId: z.string(),
3728
+ from: stageRefSchema,
3729
+ to: stageRefSchema
3730
+ })
3731
+ });
3732
+ const OpportunityWon = defineCrmEvent({
3733
+ name: CRM_EVENTS.OPPORTUNITY_WON,
3734
+ resource: "opportunity",
3735
+ description: "win() closed the deal as won (terminal).",
3736
+ zodSchema: z.object({
3737
+ opportunityId: z.string(),
3738
+ amount: z.number().optional()
3739
+ })
3740
+ });
3741
+ const OpportunityLost = defineCrmEvent({
3742
+ name: CRM_EVENTS.OPPORTUNITY_LOST,
3743
+ resource: "opportunity",
3744
+ description: "lose() closed the deal as lost (terminal).",
3745
+ zodSchema: z.object({
3746
+ opportunityId: z.string(),
3747
+ lostReasonId: z.string().optional()
3748
+ })
3749
+ });
3750
+ const OpportunityAbandoned = defineCrmEvent({
3751
+ name: CRM_EVENTS.OPPORTUNITY_ABANDONED,
3752
+ resource: "opportunity",
3753
+ description: "abandon() closed the deal as abandoned (terminal).",
3754
+ zodSchema: z.object({ opportunityId: z.string() })
3755
+ });
3756
+ const OpportunityAssigned = defineCrmEvent({
3757
+ name: CRM_EVENTS.OPPORTUNITY_ASSIGNED,
3758
+ resource: "opportunity",
3759
+ description: "assign() (re)assigned the opportunity owner.",
3760
+ zodSchema: z.object({ opportunityId: z.string() }).extend(ownerChangeSchema.shape)
3761
+ });
3762
+ const OpportunityAmountChanged = defineCrmEvent({
3763
+ name: CRM_EVENTS.OPPORTUNITY_AMOUNT_CHANGED,
3764
+ resource: "opportunity",
3765
+ description: "updateAmount() changed an open deal value.",
3766
+ zodSchema: z.object({
3767
+ opportunityId: z.string(),
3768
+ amount: z.number(),
3769
+ previousAmount: z.number().optional(),
3770
+ reason: z.string().optional(),
3771
+ by: z.string().optional()
3772
+ })
3773
+ });
3774
+ const OpportunityExtended = defineCrmEvent({
3775
+ name: CRM_EVENTS.OPPORTUNITY_EXTENDED,
3776
+ resource: "opportunity",
3777
+ description: "extendCloseDate() pushed expectedCloseAt further out.",
3778
+ zodSchema: z.object({
3779
+ opportunityId: z.string(),
3780
+ expectedCloseAt: z.iso.datetime(),
3781
+ previousExpectedCloseAt: z.iso.datetime().optional(),
3782
+ reason: z.string().optional(),
3783
+ by: z.string().optional()
3784
+ })
3785
+ });
3786
+ const PipelineCreated = defineCrmEvent({
3787
+ name: CRM_EVENTS.PIPELINE_CREATED,
3788
+ resource: "pipeline",
3789
+ description: "A pipeline was created.",
3790
+ zodSchema: z.object({ pipelineId: z.string() })
3791
+ });
3792
+ const PipelineArchived = defineCrmEvent({
3793
+ name: CRM_EVENTS.PIPELINE_ARCHIVED,
3794
+ resource: "pipeline",
3795
+ description: "archive() soft-deprecated a pipeline.",
3796
+ zodSchema: z.object({ pipelineId: z.string() })
3797
+ });
3798
+ const ActivityPlanned = defineCrmEvent({
3799
+ name: CRM_EVENTS.ACTIVITY_PLANNED,
3800
+ resource: "activity",
3801
+ description: "plan() created a planned activity against a subject.",
3802
+ zodSchema: z.object({
3803
+ activityId: z.string(),
3804
+ type: z.string(),
3805
+ subjectKind: z.string(),
3806
+ subjectId: z.string()
3807
+ })
3808
+ });
3809
+ const ActivityCompleted = defineCrmEvent({
3810
+ name: CRM_EVENTS.ACTIVITY_COMPLETED,
3811
+ resource: "activity",
3812
+ description: "complete() finished an activity.",
3813
+ zodSchema: z.object({
3814
+ activityId: z.string(),
3815
+ completedAt: z.iso.datetime()
3816
+ })
3817
+ });
3818
+ const ActivityCancelled = defineCrmEvent({
3819
+ name: CRM_EVENTS.ACTIVITY_CANCELLED,
3820
+ resource: "activity",
3821
+ description: "cancel() cancelled an activity.",
3822
+ zodSchema: z.object({
3823
+ activityId: z.string(),
3824
+ reason: z.string().optional()
3825
+ })
3826
+ });
3827
+ const ActivityRescheduled = defineCrmEvent({
3828
+ name: CRM_EVENTS.ACTIVITY_RESCHEDULED,
3829
+ resource: "activity",
3830
+ description: "reschedule() moved an activity due date.",
3831
+ zodSchema: z.object({
3832
+ activityId: z.string(),
3833
+ dueDate: z.iso.datetime(),
3834
+ previousDueDate: z.iso.datetime().optional(),
3835
+ reason: z.string().optional(),
3836
+ by: z.string().optional()
3837
+ })
3838
+ });
3839
+ const ActivityOutcomeRecorded = defineCrmEvent({
3840
+ name: CRM_EVENTS.ACTIVITY_OUTCOME_RECORDED,
3841
+ resource: "activity",
3842
+ description: "recordOutcome() logged an activity outcome (no status change).",
3843
+ zodSchema: z.object({
3844
+ activityId: z.string(),
3845
+ outcome: z.record(z.string(), z.unknown())
3846
+ })
3847
+ });
3848
+ const NoteAdded = defineCrmEvent({
3849
+ name: CRM_EVENTS.NOTE_ADDED,
3850
+ resource: "note",
3851
+ description: "A note was appended to a subject timeline.",
3852
+ zodSchema: z.object({
3853
+ noteId: z.string(),
3854
+ subjectKind: z.string(),
3855
+ subjectId: z.string()
3856
+ })
3857
+ });
3858
+ const CampaignCreated = defineCrmEvent({
3859
+ name: CRM_EVENTS.CAMPAIGN_CREATED,
3860
+ resource: "campaign",
3861
+ description: "createCampaign() inserted a campaign.",
3862
+ zodSchema: z.object({
3863
+ campaignId: z.string(),
3864
+ name: z.string()
3865
+ })
3866
+ });
3867
+ const CampaignActivated = defineCrmEvent({
3868
+ name: CRM_EVENTS.CAMPAIGN_ACTIVATED,
3869
+ resource: "campaign",
3870
+ description: "activate() moved a campaign to active.",
3871
+ zodSchema: z.object({ campaignId: z.string() })
3872
+ });
3873
+ const CampaignPaused = defineCrmEvent({
3874
+ name: CRM_EVENTS.CAMPAIGN_PAUSED,
3875
+ resource: "campaign",
3876
+ description: "pause() suspended an active campaign.",
3877
+ zodSchema: z.object({
3878
+ campaignId: z.string(),
3879
+ reason: z.string().optional()
3880
+ })
3881
+ });
3882
+ const CampaignResumed = defineCrmEvent({
3883
+ name: CRM_EVENTS.CAMPAIGN_RESUMED,
3884
+ resource: "campaign",
3885
+ description: "resume() reactivated a paused campaign.",
3886
+ zodSchema: z.object({ campaignId: z.string() })
3887
+ });
3888
+ const CampaignCompleted = defineCrmEvent({
3889
+ name: CRM_EVENTS.CAMPAIGN_COMPLETED,
3890
+ resource: "campaign",
3891
+ description: "complete() closed a campaign.",
3892
+ zodSchema: z.object({
3893
+ campaignId: z.string(),
3894
+ actualRevenueAmount: z.number().optional()
3895
+ })
3896
+ });
3897
+ const CampaignArchived = defineCrmEvent({
3898
+ name: CRM_EVENTS.CAMPAIGN_ARCHIVED,
3899
+ resource: "campaign",
3900
+ description: "archive() archived a campaign (terminal).",
3901
+ zodSchema: z.object({ campaignId: z.string() })
3902
+ });
3903
+ const SubscriptionCreated = defineCrmEvent({
3904
+ name: CRM_EVENTS.SUBSCRIPTION_CREATED,
3905
+ resource: "subscription",
3906
+ description: "subscribe() created an opt-in (pending or confirmed).",
3907
+ zodSchema: z.object({
3908
+ subscriptionId: z.string(),
3909
+ contactId: z.string(),
3910
+ campaignId: z.string(),
3911
+ requiresConfirmation: z.boolean()
3912
+ })
3913
+ });
3914
+ const SubscriptionConfirmed = defineCrmEvent({
3915
+ name: CRM_EVENTS.SUBSCRIPTION_CONFIRMED,
3916
+ resource: "subscription",
3917
+ description: "confirm() validated a double-opt-in token.",
3918
+ zodSchema: z.object({
3919
+ subscriptionId: z.string(),
3920
+ contactId: z.string(),
3921
+ campaignId: z.string(),
3922
+ confirmedAt: z.iso.datetime()
3923
+ })
3924
+ });
3925
+ const SubscriptionUnsubscribed = defineCrmEvent({
3926
+ name: CRM_EVENTS.SUBSCRIPTION_UNSUBSCRIBED,
3927
+ resource: "subscription",
3928
+ description: "unsubscribe() opted a contact out (terminal).",
3929
+ zodSchema: z.object({
3930
+ subscriptionId: z.string(),
3931
+ contactId: z.string(),
3932
+ campaignId: z.string(),
3933
+ reason: z.string().optional()
3934
+ })
3935
+ });
3936
+ const SubscriptionBounced = defineCrmEvent({
3937
+ name: CRM_EVENTS.SUBSCRIPTION_BOUNCED,
3938
+ resource: "subscription",
3939
+ description: "markBounced() recorded a hard bounce (terminal).",
3940
+ zodSchema: z.object({
3941
+ subscriptionId: z.string(),
3942
+ contactId: z.string(),
3943
+ campaignId: z.string(),
3944
+ reason: z.string().optional()
3945
+ })
3946
+ });
3947
+ const SubscriptionComplained = defineCrmEvent({
3948
+ name: CRM_EVENTS.SUBSCRIPTION_COMPLAINED,
3949
+ resource: "subscription",
3950
+ description: "markComplained() recorded a spam complaint (terminal).",
3951
+ zodSchema: z.object({
3952
+ subscriptionId: z.string(),
3953
+ contactId: z.string(),
3954
+ campaignId: z.string()
3955
+ })
3956
+ });
3957
+ const emailEventSchema = z.object({
3958
+ eventId: z.string(),
3959
+ kind: z.string(),
3960
+ subjectKind: z.string(),
3961
+ subjectId: z.string(),
3962
+ campaignId: z.string().optional()
3963
+ });
3964
+ const EmailEventOpened = defineCrmEvent({
3965
+ name: CRM_EVENTS.EMAIL_EVENT_OPENED,
3966
+ resource: "email_event",
3967
+ description: "recordOpen() logged an email open.",
3968
+ zodSchema: emailEventSchema
3969
+ });
3970
+ const EmailEventClicked = defineCrmEvent({
3971
+ name: CRM_EVENTS.EMAIL_EVENT_CLICKED,
3972
+ resource: "email_event",
3973
+ description: "recordClick() logged a link click.",
3974
+ zodSchema: emailEventSchema
3975
+ });
3976
+ const EmailEventBounced = defineCrmEvent({
3977
+ name: CRM_EVENTS.EMAIL_EVENT_BOUNCED,
3978
+ resource: "email_event",
3979
+ description: "recordBounce() logged a bounce.",
3980
+ zodSchema: emailEventSchema
3981
+ });
3982
+ const EmailEventComplained = defineCrmEvent({
3983
+ name: CRM_EVENTS.EMAIL_EVENT_COMPLAINED,
3984
+ resource: "email_event",
3985
+ description: "recordComplaint() logged a spam complaint.",
3986
+ zodSchema: emailEventSchema
3987
+ });
3988
+ const EmailEventUnsubscribed = defineCrmEvent({
3989
+ name: CRM_EVENTS.EMAIL_EVENT_UNSUBSCRIBED,
3990
+ resource: "email_event",
3991
+ description: "recordUnsubscribe() logged a one-click unsubscribe.",
3992
+ zodSchema: emailEventSchema
3993
+ });
3994
+ const ConversationCreated = defineCrmEvent({
3995
+ name: CRM_EVENTS.CONVERSATION_CREATED,
3996
+ resource: "conversation",
3997
+ description: "openConversation() started a conversation thread.",
3998
+ zodSchema: z.object({
3999
+ conversationId: z.string(),
4000
+ channel: z.string(),
4001
+ participantKind: z.string(),
4002
+ participantId: z.string()
4003
+ })
4004
+ });
4005
+ const ConversationAssigned = defineCrmEvent({
4006
+ name: CRM_EVENTS.CONVERSATION_ASSIGNED,
4007
+ resource: "conversation",
4008
+ description: "assign() (re)assigned a conversation owner.",
4009
+ zodSchema: z.object({
4010
+ conversationId: z.string(),
4011
+ assigneeId: z.string(),
4012
+ previousAssigneeId: z.string().optional()
4013
+ })
4014
+ });
4015
+ const ConversationResolved = defineCrmEvent({
4016
+ name: CRM_EVENTS.CONVERSATION_RESOLVED,
4017
+ resource: "conversation",
4018
+ description: "resolve() closed a conversation.",
4019
+ zodSchema: z.object({
4020
+ conversationId: z.string(),
4021
+ status: z.string(),
4022
+ previousStatus: z.string()
4023
+ })
4024
+ });
4025
+ const ConversationReopened = defineCrmEvent({
4026
+ name: CRM_EVENTS.CONVERSATION_REOPENED,
4027
+ resource: "conversation",
4028
+ description: "reopen() reopened a resolved conversation.",
4029
+ zodSchema: z.object({
4030
+ conversationId: z.string(),
4031
+ status: z.string(),
4032
+ previousStatus: z.string()
4033
+ })
4034
+ });
4035
+ const MessageReceived = defineCrmEvent({
4036
+ name: CRM_EVENTS.MESSAGE_RECEIVED,
4037
+ resource: "message",
4038
+ description: "recordInbound() logged an inbound message.",
4039
+ zodSchema: z.object({
4040
+ messageId: z.string(),
4041
+ conversationId: z.string(),
4042
+ direction: z.string(),
4043
+ externalMessageId: z.string().optional()
4044
+ })
4045
+ });
4046
+ const MessageSent = defineCrmEvent({
4047
+ name: CRM_EVENTS.MESSAGE_SENT,
4048
+ resource: "message",
4049
+ description: "recordOutbound() logged an outbound message.",
4050
+ zodSchema: z.object({
4051
+ messageId: z.string(),
4052
+ conversationId: z.string(),
4053
+ direction: z.string(),
4054
+ externalMessageId: z.string().optional()
4055
+ })
4056
+ });
4057
+ const crmEventDefinitions = [
4058
+ LeadCreated,
4059
+ LeadStatusChanged,
4060
+ LeadScored,
4061
+ LeadConverted,
4062
+ LeadDisqualified,
4063
+ LeadEnriched,
4064
+ LeadAssigned,
4065
+ AccountCreated,
4066
+ AccountUpdated,
4067
+ AccountEnriched,
4068
+ AccountAssigned,
4069
+ AccountMerged,
4070
+ ContactCreated,
4071
+ ContactUpdated,
4072
+ ContactEnriched,
4073
+ ContactAssigned,
4074
+ ContactMerged,
4075
+ OpportunityCreated,
4076
+ OpportunityStageAdvanced,
4077
+ OpportunityStageRegressed,
4078
+ OpportunityWon,
4079
+ OpportunityLost,
4080
+ OpportunityAbandoned,
4081
+ OpportunityAssigned,
4082
+ OpportunityAmountChanged,
4083
+ OpportunityExtended,
4084
+ PipelineCreated,
4085
+ PipelineArchived,
4086
+ ActivityPlanned,
4087
+ ActivityCompleted,
4088
+ ActivityCancelled,
4089
+ ActivityRescheduled,
4090
+ ActivityOutcomeRecorded,
4091
+ NoteAdded,
4092
+ CampaignCreated,
4093
+ CampaignActivated,
4094
+ CampaignPaused,
4095
+ CampaignResumed,
4096
+ CampaignCompleted,
4097
+ CampaignArchived,
4098
+ SubscriptionCreated,
4099
+ SubscriptionConfirmed,
4100
+ SubscriptionUnsubscribed,
4101
+ SubscriptionBounced,
4102
+ SubscriptionComplained,
4103
+ EmailEventOpened,
4104
+ EmailEventClicked,
4105
+ EmailEventBounced,
4106
+ EmailEventComplained,
4107
+ EmailEventUnsubscribed,
4108
+ ConversationCreated,
4109
+ ConversationAssigned,
4110
+ ConversationResolved,
4111
+ ConversationReopened,
4112
+ MessageReceived,
4113
+ MessageSent
4114
+ ];
4115
+ //#endregion
3403
4116
  //#region src/shared/calculators/forecast.ts
3404
4117
  /**
3405
4118
  * Aggregate opportunities into a forecast table — one row per bucket.