@classytic/crm 1.0.0 → 1.1.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/CHANGELOG.md CHANGED
@@ -2,7 +2,19 @@
2
2
 
3
3
  All notable changes to `@classytic/crm` are documented here.
4
4
 
5
- ## 1.0.0 - 2026-07-29 (unpublished — last published is 0.9.0)
5
+ ## 1.1.1 - 2026-08-20
6
+
7
+ ### Fixed
8
+ - `RmaEventSchema` (and equivalent CRM event schemas) declare `type: 'object'` so hosts can register them without a cast.
9
+
10
+ ## 1.1.0 - 2026-08-17
11
+
12
+ ### Changed
13
+ - `tenant` config key replaces `tenantFieldType` across all models and engine (`assertNoLegacyTenantKeys` guard applied)
14
+ - `OpportunityRepository` gains new query methods
15
+ - Model schemas standardised (consistent field ordering, injected tenant field)
16
+
17
+ ## 1.0.0 - 2026-07-29
6
18
 
7
19
  ### Fixed — Lead and Contact forked one mailbox into two rows (BEHAVIOURAL, 2026-08-08)
8
20
 
package/README.md CHANGED
@@ -99,7 +99,7 @@ import { createCrm, LeadService } from '@classytic/crm';
99
99
 
100
100
  const crm = await createCrm({
101
101
  connection: mongoose.connection,
102
- tenantFieldType: 'objectId', // matches Better Auth orgs; 'string' for UUID hosts
102
+ tenant: { fieldType: 'objectId' }, // matches Better Auth orgs; 'string' for UUID hosts
103
103
  });
104
104
 
105
105
  const ctx = { organizationId: orgId };
package/dist/events.d.ts CHANGED
@@ -5,6 +5,7 @@ import "mongoose";
5
5
  import "@classytic/mongokit";
6
6
  import { OperationContext } from "@classytic/primitives/context";
7
7
  import "@classytic/primitives/outbox";
8
+ import "@classytic/repo-core/tenant";
8
9
  //#region src/events/event-constants.d.ts
9
10
  /** PACKAGE_RULES rule 12: `package:resource.verb`. */
10
11
  declare const CRM_EVENTS: {
@@ -82,9 +83,33 @@ declare const CRM_EVENTS: {
82
83
  type CrmEventName = (typeof CRM_EVENTS)[keyof typeof CRM_EVENTS];
83
84
  //#endregion
84
85
  //#region src/events/event-catalog.d.ts
85
- /** JSON-Schema object produced by `z.toJSONSchema` — what arc's registry consumes. */
86
- type CrmEventSchema = Record<string, unknown>;
87
- /** Structurally identical to arc's `EventDefinitionOutput` — do NOT import arc. */
86
+ /**
87
+ * JSON-Schema object produced by `z.toJSONSchema` — what arc's registry consumes.
88
+ *
89
+ * `type` is DECLARED. This was `Record<string, unknown>`, which is neither what
90
+ * `z.toJSONSchema` returns for an object schema nor what arc's `EventSchema`
91
+ * accepts — arc requires `type: 'object'`. So the comment below claiming
92
+ * structural identity with arc's `EventDefinitionOutput` was false, and a host
93
+ * registering this catalog got a type error whose only cheap escapes were a
94
+ * cast or re-declaring the events locally. Every runtime value already carries
95
+ * `type: 'object'`; only the declaration was loose.
96
+ *
97
+ * Same fix, same reason, as `@classytic/transfer`'s `TransferEventSchema` —
98
+ * read the note there. Enforcement is at the CONSUMER: be-prod's
99
+ * `shared/event-registry.ts` hands these straight to arc, so drift fails that
100
+ * build. A kernel must not import arc, which is why the claim can only be
101
+ * checked where both are visible.
102
+ */
103
+ interface CrmEventSchema {
104
+ type: 'object';
105
+ properties?: Record<string, {
106
+ type?: string;
107
+ [key: string]: unknown;
108
+ }>;
109
+ required?: string[];
110
+ [key: string]: unknown;
111
+ }
112
+ /** Structurally assignable to arc's `EventDefinitionOutput` — do NOT import arc. */
88
113
  interface CrmEventDefinition<TSchema extends z.ZodType = z.ZodType> {
89
114
  name: CrmEventName;
90
115
  version: number;
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ import { ModelBlueprint, PaginationConfig, PluginType, Repository } from "@class
2
2
  import { Connection, Model, Schema, SchemaDefinitionProperty } from "mongoose";
3
3
  import { DomainEvent, EventHandler, EventMeta, EventTransport } from "@classytic/primitives/events";
4
4
  import { InProcessEventBus } from "@classytic/primitives/event-infra";
5
+ import { ResolvedTenantConfig, TenantConfig } from "@classytic/repo-core/tenant";
5
6
  import { z } from "zod";
6
7
  import { StatusChangeEntry, StatusHistory, appendStatus, lastTransitionTo, latestEntry, timeInStatus } from "@classytic/primitives/status-history";
7
8
  import { OperationContext } from "@classytic/primitives/context";
@@ -83,10 +84,9 @@ declare const ALL_CRM_MODULES: ResolvedCrmModules;
83
84
  declare function resolveCrmModules(modules: CrmModulesConfig | undefined): ResolvedCrmModules;
84
85
  //#endregion
85
86
  //#region src/models/tenant-field.d.ts
86
- type TenantFieldType = 'objectId' | 'string';
87
87
  interface SchemaBuildConfig {
88
- /** Schema type for the tenant column. Engine plumbs this everywhere. */
89
- tenantFieldType: TenantFieldType;
88
+ /** Canonical resolved tenant config. Engine plumbs this everywhere. */
89
+ tenant: ResolvedTenantConfig;
90
90
  /** Optional Mongoose model namespace for hosts with generic model names. */
91
91
  modelPrefix?: string | undefined;
92
92
  }
@@ -1515,19 +1515,13 @@ interface CrmShape {
1515
1515
  * Threaded into both schemas and `multiTenantPlugin` so they stay in
1516
1516
  * sync (PACKAGE_RULES §9.2).
1517
1517
  */
1518
- tenantFieldType?: TenantFieldType | undefined;
1518
+ tenant?: boolean | TenantConfig | undefined;
1519
1519
  /**
1520
1520
  * Prefix for Mongoose model registry names (for example `Crm` produces
1521
1521
  * `CrmAccount`). Use in large hosts where generic names such as `Account`
1522
1522
  * are already owned by accounting or identity modules.
1523
1523
  */
1524
1524
  modelPrefix?: string | undefined;
1525
- /**
1526
- * When `true` (default), wire `multiTenantPlugin` on every repository
1527
- * with `required: true`. Set `false` only for fixtures / smoke tests
1528
- * that intentionally drop the scope.
1529
- */
1530
- multiTenant?: boolean | undefined;
1531
1525
  /**
1532
1526
  * Progressive feature bundles (PACKAGE_RULES §22). Omit for the
1533
1527
  * all-enabled engine. When provided, unspecified modules are disabled;
@@ -1578,8 +1572,7 @@ interface CrmRuntime {
1578
1572
  }
1579
1573
  interface ResolvedCrmConfig extends SchemaBuildConfig {
1580
1574
  connection: Connection;
1581
- tenantFieldType: TenantFieldType;
1582
- multiTenant: boolean;
1575
+ tenant: ResolvedTenantConfig;
1583
1576
  forceRecreate: boolean;
1584
1577
  autoIndex: boolean;
1585
1578
  modules: ResolvedCrmModules;
@@ -1862,9 +1855,33 @@ declare const CRM_EVENTS: {
1862
1855
  type CrmEventName = (typeof CRM_EVENTS)[keyof typeof CRM_EVENTS];
1863
1856
  //#endregion
1864
1857
  //#region src/events/event-catalog.d.ts
1865
- /** JSON-Schema object produced by `z.toJSONSchema` — what arc's registry consumes. */
1866
- type CrmEventSchema = Record<string, unknown>;
1867
- /** Structurally identical to arc's `EventDefinitionOutput` — do NOT import arc. */
1858
+ /**
1859
+ * JSON-Schema object produced by `z.toJSONSchema` — what arc's registry consumes.
1860
+ *
1861
+ * `type` is DECLARED. This was `Record<string, unknown>`, which is neither what
1862
+ * `z.toJSONSchema` returns for an object schema nor what arc's `EventSchema`
1863
+ * accepts — arc requires `type: 'object'`. So the comment below claiming
1864
+ * structural identity with arc's `EventDefinitionOutput` was false, and a host
1865
+ * registering this catalog got a type error whose only cheap escapes were a
1866
+ * cast or re-declaring the events locally. Every runtime value already carries
1867
+ * `type: 'object'`; only the declaration was loose.
1868
+ *
1869
+ * Same fix, same reason, as `@classytic/transfer`'s `TransferEventSchema` —
1870
+ * read the note there. Enforcement is at the CONSUMER: be-prod's
1871
+ * `shared/event-registry.ts` hands these straight to arc, so drift fails that
1872
+ * build. A kernel must not import arc, which is why the claim can only be
1873
+ * checked where both are visible.
1874
+ */
1875
+ interface CrmEventSchema {
1876
+ type: 'object';
1877
+ properties?: Record<string, {
1878
+ type?: string;
1879
+ [key: string]: unknown;
1880
+ }>;
1881
+ required?: string[];
1882
+ [key: string]: unknown;
1883
+ }
1884
+ /** Structurally assignable to arc's `EventDefinitionOutput` — do NOT import arc. */
1868
1885
  interface CrmEventDefinition<TSchema extends z.ZodType = z.ZodType> {
1869
1886
  name: CrmEventName;
1870
1887
  version: number;
@@ -3203,4 +3220,4 @@ declare namespace index_d_exports {
3203
3220
  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 };
3204
3221
  }
3205
3222
  //#endregion
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 };
3223
+ 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, 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 };
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ import { createEvent as createEvent$1 } from "@classytic/primitives/events";
6
6
  import { IllegalTransitionError, defineStateMachine } from "@classytic/primitives/state-machine";
7
7
  import { randomUUID } from "node:crypto";
8
8
  import { InProcessEventBus } from "@classytic/primitives/event-infra";
9
+ import { assertNoLegacyTenantKeys, resolveTenantConfig } from "@classytic/repo-core/tenant";
9
10
  import { z } from "zod";
10
11
  import { civilDateOf } from "@classytic/primitives/timezone";
11
12
  import { CURRENCY_PATTERN } from "@classytic/primitives/currency";
@@ -51,15 +52,14 @@ function crmModelName(config, base) {
51
52
  * `organizationId` — never write the raw type in a schema file.
52
53
  */
53
54
  function tenantFieldDef(config) {
54
- return config.tenantFieldType === "objectId" ? {
55
+ const { tenant } = config;
56
+ return tenant.fieldType === "objectId" ? {
55
57
  type: Schema.Types.ObjectId,
56
- required: true,
57
- ref: "organization",
58
- index: true
58
+ ...tenant.enabled && tenant.required ? { required: true } : {},
59
+ ...tenant.ref ? { ref: tenant.ref } : {}
59
60
  } : {
60
61
  type: String,
61
- required: true,
62
- index: true
62
+ ...tenant.enabled && tenant.required ? { required: true } : {}
63
63
  };
64
64
  }
65
65
  //#endregion
@@ -105,7 +105,7 @@ const stageSubSchema = new Schema({
105
105
  }, { _id: true });
106
106
  function buildPipelineSchema(config) {
107
107
  const schema = new Schema({
108
- organizationId: tenantFieldDef(config),
108
+ [config.tenant.tenantField]: tenantFieldDef(config),
109
109
  name: {
110
110
  type: String,
111
111
  required: true,
@@ -129,11 +129,11 @@ function buildPipelineSchema(config) {
129
129
  metadata: { type: Schema.Types.Mixed }
130
130
  }, { timestamps: true });
131
131
  schema.index({
132
- organizationId: 1,
132
+ [config.tenant.tenantField]: 1,
133
133
  isArchived: 1
134
134
  });
135
135
  schema.index({
136
- organizationId: 1,
136
+ [config.tenant.tenantField]: 1,
137
137
  name: 1
138
138
  });
139
139
  return schema;
@@ -539,7 +539,7 @@ const engagementSchema = new Schema({
539
539
  }, { _id: false });
540
540
  function buildLeadSchema(config) {
541
541
  const schema = new Schema({
542
- organizationId: tenantFieldDef(config),
542
+ [config.tenant.tenantField]: tenantFieldDef(config),
543
543
  fullName: {
544
544
  type: String,
545
545
  required: true,
@@ -634,18 +634,18 @@ function buildLeadSchema(config) {
634
634
  metadata: { type: Schema.Types.Mixed }
635
635
  }, { timestamps: true });
636
636
  schema.index({
637
- organizationId: 1,
637
+ [config.tenant.tenantField]: 1,
638
638
  email: 1
639
639
  }, {
640
640
  unique: true,
641
641
  partialFilterExpression: { email: { $type: "string" } }
642
642
  });
643
643
  schema.index({
644
- organizationId: 1,
644
+ [config.tenant.tenantField]: 1,
645
645
  createdAt: -1
646
646
  });
647
647
  schema.index({
648
- organizationId: 1,
648
+ [config.tenant.tenantField]: 1,
649
649
  score: -1
650
650
  });
651
651
  return schema;
@@ -705,7 +705,7 @@ const statusEntrySchema$4 = new Schema({
705
705
  }, { _id: false });
706
706
  function buildOpportunitySchema(config) {
707
707
  const schema = new Schema({
708
- organizationId: tenantFieldDef(config),
708
+ [config.tenant.tenantField]: tenantFieldDef(config),
709
709
  name: {
710
710
  type: String,
711
711
  required: true,
@@ -775,17 +775,17 @@ function buildOpportunitySchema(config) {
775
775
  metadata: { type: Schema.Types.Mixed }
776
776
  }, { timestamps: true });
777
777
  schema.index({
778
- organizationId: 1,
778
+ [config.tenant.tenantField]: 1,
779
779
  pipelineId: 1,
780
780
  stageId: 1
781
781
  });
782
782
  schema.index({
783
- organizationId: 1,
783
+ [config.tenant.tenantField]: 1,
784
784
  status: 1,
785
785
  expectedCloseAt: 1
786
786
  });
787
787
  schema.index({
788
- organizationId: 1,
788
+ [config.tenant.tenantField]: 1,
789
789
  ownerId: 1,
790
790
  status: 1
791
791
  });
@@ -884,7 +884,7 @@ const contactInfoSchema$1 = new Schema({
884
884
  }, { _id: false });
885
885
  function buildContactSchema(config) {
886
886
  const schema = new Schema({
887
- organizationId: tenantFieldDef(config),
887
+ [config.tenant.tenantField]: tenantFieldDef(config),
888
888
  name: {
889
889
  type: nameSchema,
890
890
  required: true
@@ -918,22 +918,22 @@ function buildContactSchema(config) {
918
918
  toObject: { virtuals: true }
919
919
  });
920
920
  schema.index({
921
- organizationId: 1,
921
+ [config.tenant.tenantField]: 1,
922
922
  "contact.email": 1
923
923
  }, {
924
924
  unique: true,
925
925
  partialFilterExpression: { "contact.email": { $type: "string" } }
926
926
  });
927
927
  schema.index({
928
- organizationId: 1,
928
+ [config.tenant.tenantField]: 1,
929
929
  accountId: 1
930
930
  });
931
931
  schema.index({
932
- organizationId: 1,
932
+ [config.tenant.tenantField]: 1,
933
933
  createdAt: -1
934
934
  });
935
935
  schema.index({
936
- organizationId: 1,
936
+ [config.tenant.tenantField]: 1,
937
937
  tags: 1
938
938
  });
939
939
  schema.virtual("fullName").get(function() {
@@ -954,7 +954,7 @@ function createContactModel(connection, config) {
954
954
  */
955
955
  function buildAccountSchema(config) {
956
956
  const schema = new Schema({
957
- organizationId: tenantFieldDef(config),
957
+ [config.tenant.tenantField]: tenantFieldDef(config),
958
958
  name: {
959
959
  type: String,
960
960
  required: true,
@@ -993,15 +993,15 @@ function buildAccountSchema(config) {
993
993
  metadata: { type: Schema.Types.Mixed }
994
994
  }, { timestamps: true });
995
995
  schema.index({
996
- organizationId: 1,
996
+ [config.tenant.tenantField]: 1,
997
997
  domain: 1
998
998
  });
999
999
  schema.index({
1000
- organizationId: 1,
1000
+ [config.tenant.tenantField]: 1,
1001
1001
  createdAt: -1
1002
1002
  });
1003
1003
  schema.index({
1004
- organizationId: 1,
1004
+ [config.tenant.tenantField]: 1,
1005
1005
  tags: 1
1006
1006
  });
1007
1007
  return schema;
@@ -1067,7 +1067,7 @@ const statusEntrySchema$3 = new Schema({
1067
1067
  }, { _id: false });
1068
1068
  function buildCampaignSchema(config) {
1069
1069
  const schema = new Schema({
1070
- organizationId: tenantFieldDef(config),
1070
+ [config.tenant.tenantField]: tenantFieldDef(config),
1071
1071
  name: {
1072
1072
  type: String,
1073
1073
  required: true,
@@ -1124,23 +1124,23 @@ function buildCampaignSchema(config) {
1124
1124
  metadata: { type: Schema.Types.Mixed }
1125
1125
  }, { timestamps: true });
1126
1126
  schema.index({
1127
- organizationId: 1,
1127
+ [config.tenant.tenantField]: 1,
1128
1128
  name: 1
1129
1129
  }, { unique: true });
1130
1130
  schema.index({
1131
- organizationId: 1,
1131
+ [config.tenant.tenantField]: 1,
1132
1132
  status: 1
1133
1133
  });
1134
1134
  schema.index({
1135
- organizationId: 1,
1135
+ [config.tenant.tenantField]: 1,
1136
1136
  startDate: 1
1137
1137
  });
1138
1138
  schema.index({
1139
- organizationId: 1,
1139
+ [config.tenant.tenantField]: 1,
1140
1140
  createdAt: -1
1141
1141
  });
1142
1142
  schema.index({
1143
- organizationId: 1,
1143
+ [config.tenant.tenantField]: 1,
1144
1144
  tags: 1
1145
1145
  });
1146
1146
  return schema;
@@ -1216,7 +1216,7 @@ const statusEntrySchema$2 = new Schema({
1216
1216
  }, { _id: false });
1217
1217
  function buildSubscriptionSchema(config) {
1218
1218
  const schema = new Schema({
1219
- organizationId: tenantFieldDef(config),
1219
+ [config.tenant.tenantField]: tenantFieldDef(config),
1220
1220
  contactId: {
1221
1221
  type: Schema.Types.ObjectId,
1222
1222
  ref: crmModelName(config, "Contact"),
@@ -1285,12 +1285,12 @@ function buildSubscriptionSchema(config) {
1285
1285
  collection: "email_subscriptions"
1286
1286
  });
1287
1287
  schema.index({
1288
- organizationId: 1,
1288
+ [config.tenant.tenantField]: 1,
1289
1289
  campaignId: 1,
1290
1290
  status: 1
1291
1291
  });
1292
1292
  schema.index({
1293
- organizationId: 1,
1293
+ [config.tenant.tenantField]: 1,
1294
1294
  contactId: 1,
1295
1295
  campaignId: 1
1296
1296
  }, {
@@ -1298,7 +1298,7 @@ function buildSubscriptionSchema(config) {
1298
1298
  partialFilterExpression: { status: { $in: ["pending", "confirmed"] } }
1299
1299
  });
1300
1300
  schema.index({
1301
- organizationId: 1,
1301
+ [config.tenant.tenantField]: 1,
1302
1302
  createdAt: -1
1303
1303
  });
1304
1304
  return schema;
@@ -1335,7 +1335,7 @@ const SUPPRESSION_REASONS = [...EVENT_DERIVED_SUPPRESSION_REASONS, "manual"];
1335
1335
  */
1336
1336
  function buildSuppressionSchema(config) {
1337
1337
  const schema = new Schema({
1338
- organizationId: tenantFieldDef(config),
1338
+ [config.tenant.tenantField]: tenantFieldDef(config),
1339
1339
  email: {
1340
1340
  type: String,
1341
1341
  required: true,
@@ -1371,16 +1371,16 @@ function buildSuppressionSchema(config) {
1371
1371
  metadata: { type: Schema.Types.Mixed }
1372
1372
  }, { timestamps: true });
1373
1373
  schema.index({
1374
- organizationId: 1,
1374
+ [config.tenant.tenantField]: 1,
1375
1375
  email: 1
1376
1376
  }, { unique: true });
1377
1377
  schema.index({
1378
- organizationId: 1,
1378
+ [config.tenant.tenantField]: 1,
1379
1379
  reason: 1,
1380
1380
  suppressedAt: -1
1381
1381
  });
1382
1382
  schema.index({
1383
- organizationId: 1,
1383
+ [config.tenant.tenantField]: 1,
1384
1384
  suppressedAt: -1
1385
1385
  });
1386
1386
  return schema;
@@ -1441,7 +1441,7 @@ const SUBJECT_KINDS = [
1441
1441
  */
1442
1442
  function buildActivitySchema(config) {
1443
1443
  const schema = new Schema({
1444
- organizationId: tenantFieldDef(config),
1444
+ [config.tenant.tenantField]: tenantFieldDef(config),
1445
1445
  type: {
1446
1446
  type: String,
1447
1447
  enum: ACTIVITY_TYPES,
@@ -1496,26 +1496,26 @@ function buildActivitySchema(config) {
1496
1496
  metadata: { type: Schema.Types.Mixed }
1497
1497
  }, { timestamps: true });
1498
1498
  schema.index({
1499
- organizationId: 1,
1499
+ [config.tenant.tenantField]: 1,
1500
1500
  subjectKind: 1,
1501
1501
  subjectId: 1
1502
1502
  });
1503
1503
  schema.index({
1504
- organizationId: 1,
1504
+ [config.tenant.tenantField]: 1,
1505
1505
  status: 1,
1506
1506
  dueDate: 1
1507
1507
  });
1508
1508
  schema.index({
1509
- organizationId: 1,
1509
+ [config.tenant.tenantField]: 1,
1510
1510
  ownerId: 1,
1511
1511
  status: 1
1512
1512
  });
1513
1513
  schema.index({
1514
- organizationId: 1,
1514
+ [config.tenant.tenantField]: 1,
1515
1515
  type: 1
1516
1516
  });
1517
1517
  schema.index({
1518
- organizationId: 1,
1518
+ [config.tenant.tenantField]: 1,
1519
1519
  createdAt: -1
1520
1520
  });
1521
1521
  return schema;
@@ -1533,7 +1533,7 @@ function createActivityModel(connection, config) {
1533
1533
  */
1534
1534
  function buildNoteSchema(config) {
1535
1535
  const schema = new Schema({
1536
- organizationId: tenantFieldDef(config),
1536
+ [config.tenant.tenantField]: tenantFieldDef(config),
1537
1537
  subjectKind: {
1538
1538
  type: String,
1539
1539
  enum: SUBJECT_KINDS,
@@ -1562,7 +1562,7 @@ function buildNoteSchema(config) {
1562
1562
  metadata: { type: Schema.Types.Mixed }
1563
1563
  }, { timestamps: true });
1564
1564
  schema.index({
1565
- organizationId: 1,
1565
+ [config.tenant.tenantField]: 1,
1566
1566
  subjectKind: 1,
1567
1567
  subjectId: 1,
1568
1568
  pinned: -1,
@@ -1583,7 +1583,7 @@ function createNoteModel(connection, config) {
1583
1583
  */
1584
1584
  function buildLossReasonSchema(config) {
1585
1585
  const schema = new Schema({
1586
- organizationId: tenantFieldDef(config),
1586
+ [config.tenant.tenantField]: tenantFieldDef(config),
1587
1587
  name: {
1588
1588
  type: String,
1589
1589
  required: true,
@@ -1607,7 +1607,7 @@ function buildLossReasonSchema(config) {
1607
1607
  metadata: { type: Schema.Types.Mixed }
1608
1608
  }, { timestamps: true });
1609
1609
  schema.index({
1610
- organizationId: 1,
1610
+ [config.tenant.tenantField]: 1,
1611
1611
  isActive: 1,
1612
1612
  sequence: 1
1613
1613
  });
@@ -1664,7 +1664,7 @@ const EMAIL_EVENT_KINDS = [
1664
1664
  const EMAIL_EVENT_SUBJECT_KINDS = ["contact", "subscription"];
1665
1665
  function buildEmailEventSchema(config) {
1666
1666
  const schema = new Schema({
1667
- organizationId: tenantFieldDef(config),
1667
+ [config.tenant.tenantField]: tenantFieldDef(config),
1668
1668
  kind: {
1669
1669
  type: String,
1670
1670
  enum: EMAIL_EVENT_KINDS,
@@ -1712,23 +1712,23 @@ function buildEmailEventSchema(config) {
1712
1712
  updatedAt: false
1713
1713
  } });
1714
1714
  schema.index({
1715
- organizationId: 1,
1715
+ [config.tenant.tenantField]: 1,
1716
1716
  createdAt: -1
1717
1717
  });
1718
1718
  schema.index({
1719
- organizationId: 1,
1719
+ [config.tenant.tenantField]: 1,
1720
1720
  subjectKind: 1,
1721
1721
  subjectId: 1,
1722
1722
  createdAt: -1
1723
1723
  });
1724
1724
  schema.index({
1725
- organizationId: 1,
1725
+ [config.tenant.tenantField]: 1,
1726
1726
  campaignId: 1,
1727
1727
  kind: 1,
1728
1728
  createdAt: -1
1729
1729
  });
1730
1730
  schema.index({
1731
- organizationId: 1,
1731
+ [config.tenant.tenantField]: 1,
1732
1732
  messageId: 1
1733
1733
  });
1734
1734
  return schema;
@@ -1783,7 +1783,7 @@ const MESSAGE_DELIVERY_STATUSES = [
1783
1783
  */
1784
1784
  function buildConversationSchema(config) {
1785
1785
  const schema = new Schema({
1786
- organizationId: tenantFieldDef(config),
1786
+ [config.tenant.tenantField]: tenantFieldDef(config),
1787
1787
  channel: {
1788
1788
  type: String,
1789
1789
  enum: CONVERSATION_CHANNELS,
@@ -1827,22 +1827,22 @@ function buildConversationSchema(config) {
1827
1827
  metadata: { type: Schema.Types.Mixed }
1828
1828
  }, { timestamps: true });
1829
1829
  schema.index({
1830
- organizationId: 1,
1830
+ [config.tenant.tenantField]: 1,
1831
1831
  participantKind: 1,
1832
1832
  participantId: 1
1833
1833
  });
1834
1834
  schema.index({
1835
- organizationId: 1,
1835
+ [config.tenant.tenantField]: 1,
1836
1836
  status: 1,
1837
1837
  lastMessageAt: -1
1838
1838
  });
1839
1839
  schema.index({
1840
- organizationId: 1,
1840
+ [config.tenant.tenantField]: 1,
1841
1841
  assigneeId: 1,
1842
1842
  status: 1
1843
1843
  });
1844
1844
  schema.index({
1845
- organizationId: 1,
1845
+ [config.tenant.tenantField]: 1,
1846
1846
  channel: 1,
1847
1847
  channelUserId: 1
1848
1848
  });
@@ -1890,7 +1890,7 @@ const attachmentSchema = new Schema({
1890
1890
  }, { _id: false });
1891
1891
  function buildMessageSchema(config) {
1892
1892
  const schema = new Schema({
1893
- organizationId: tenantFieldDef(config),
1893
+ [config.tenant.tenantField]: tenantFieldDef(config),
1894
1894
  conversationId: {
1895
1895
  type: Schema.Types.ObjectId,
1896
1896
  ref: crmModelName(config, "Conversation"),
@@ -1935,12 +1935,12 @@ function buildMessageSchema(config) {
1935
1935
  metadata: { type: Schema.Types.Mixed }
1936
1936
  }, { timestamps: true });
1937
1937
  schema.index({
1938
- organizationId: 1,
1938
+ [config.tenant.tenantField]: 1,
1939
1939
  conversationId: 1,
1940
1940
  createdAt: -1
1941
1941
  });
1942
1942
  schema.index({
1943
- organizationId: 1,
1943
+ [config.tenant.tenantField]: 1,
1944
1944
  externalMessageId: 1
1945
1945
  }, {
1946
1946
  unique: true,
@@ -2005,7 +2005,7 @@ const statusEntrySchema$1 = new Schema({
2005
2005
  }, { _id: false });
2006
2006
  function buildSequenceSchema(config) {
2007
2007
  const schema = new Schema({
2008
- organizationId: tenantFieldDef(config),
2008
+ [config.tenant.tenantField]: tenantFieldDef(config),
2009
2009
  name: {
2010
2010
  type: String,
2011
2011
  required: true,
@@ -2053,11 +2053,11 @@ function buildSequenceSchema(config) {
2053
2053
  }
2054
2054
  }, { timestamps: true });
2055
2055
  schema.index({
2056
- organizationId: 1,
2056
+ [config.tenant.tenantField]: 1,
2057
2057
  status: 1
2058
2058
  });
2059
2059
  schema.index({
2060
- organizationId: 1,
2060
+ [config.tenant.tenantField]: 1,
2061
2061
  createdAt: -1
2062
2062
  });
2063
2063
  return schema;
@@ -2137,9 +2137,9 @@ const statusEntrySchema = new Schema({
2137
2137
  }, { _id: false });
2138
2138
  function buildEnrollmentSchema(config) {
2139
2139
  const schema = new Schema({
2140
- organizationId: tenantFieldDef(config),
2140
+ [config.tenant.tenantField]: tenantFieldDef(config),
2141
2141
  sequenceId: {
2142
- type: config.tenantFieldType === "objectId" ? Schema.Types.ObjectId : String,
2142
+ type: config.tenant.fieldType === "objectId" ? Schema.Types.ObjectId : String,
2143
2143
  required: true,
2144
2144
  ref: crmModelName(config, "Sequence")
2145
2145
  },
@@ -2149,7 +2149,7 @@ function buildEnrollmentSchema(config) {
2149
2149
  required: true
2150
2150
  },
2151
2151
  subjectId: {
2152
- type: config.tenantFieldType === "objectId" ? Schema.Types.ObjectId : String,
2152
+ type: config.tenant.fieldType === "objectId" ? Schema.Types.ObjectId : String,
2153
2153
  required: true
2154
2154
  },
2155
2155
  status: {
@@ -2193,23 +2193,23 @@ function buildEnrollmentSchema(config) {
2193
2193
  }
2194
2194
  }, { timestamps: true });
2195
2195
  schema.index({
2196
- organizationId: 1,
2196
+ [config.tenant.tenantField]: 1,
2197
2197
  sequenceId: 1,
2198
2198
  subjectId: 1,
2199
2199
  status: 1
2200
2200
  }, { unique: false });
2201
2201
  schema.index({
2202
- organizationId: 1,
2202
+ [config.tenant.tenantField]: 1,
2203
2203
  sequenceId: 1,
2204
2204
  status: 1
2205
2205
  });
2206
2206
  schema.index({
2207
- organizationId: 1,
2207
+ [config.tenant.tenantField]: 1,
2208
2208
  subjectKind: 1,
2209
2209
  subjectId: 1
2210
2210
  });
2211
2211
  schema.index({
2212
- organizationId: 1,
2212
+ [config.tenant.tenantField]: 1,
2213
2213
  createdAt: -1
2214
2214
  });
2215
2215
  return schema;
@@ -3089,8 +3089,33 @@ var OpportunityRepository = class extends Repository {
3089
3089
  opportunityId: id
3090
3090
  });
3091
3091
  if (amount === existing.amount) return existing;
3092
- const updated = await this.update(id, { amount }, ctx);
3093
- if (!updated) throw new NotFoundError("Opportunity", id);
3092
+ /**
3093
+ * `claim` with from === to — the status predicate rides the WRITE's filter.
3094
+ *
3095
+ * The `status === 'open'` check above is read-then-write: an opportunity closed
3096
+ * between the two still had its amount changed, so the figure that fed closed-won
3097
+ * reporting depended on scheduling. Not money movement, but the forecast is what
3098
+ * pipeline decisions are made against, and a closed deal whose amount silently
3099
+ * moves after the close is unauditable. The miss disambiguates below: a genuine
3100
+ * status change gets the SAME transition error the pre-check throws, so callers
3101
+ * see one contract whether they lost a race or arrived late.
3102
+ */
3103
+ const updated = await this.claim(id, {
3104
+ from: "open",
3105
+ to: "open"
3106
+ }, { $set: { amount } }, ctx);
3107
+ if (!updated) {
3108
+ const current = await this.getById(id, {
3109
+ ...ctx,
3110
+ throwOnNotFound: false
3111
+ });
3112
+ if (!current) throw new NotFoundError("Opportunity", id);
3113
+ throw new InvalidOpportunityTransitionError({
3114
+ from: current.status,
3115
+ to: current.status,
3116
+ opportunityId: id
3117
+ });
3118
+ }
3094
3119
  await this.events.dispatch(ctx, createEvent(CRM_EVENTS.OPPORTUNITY_AMOUNT_CHANGED, {
3095
3120
  opportunityId: id,
3096
3121
  amount,
@@ -4497,11 +4522,11 @@ function createCrmRepositories(models, deps) {
4497
4522
  const { events, config } = deps;
4498
4523
  const pagination = config.pagination;
4499
4524
  const plugins = [methodRegistryPlugin()];
4500
- if (config.multiTenant) plugins.push(multiTenantPlugin({
4501
- tenantField: "organizationId",
4502
- contextKey: "organizationId",
4503
- required: true,
4504
- fieldType: config.tenantFieldType
4525
+ if (config.tenant.enabled) plugins.push(multiTenantPlugin({
4526
+ tenantField: config.tenant.tenantField,
4527
+ contextKey: config.tenant.contextKey,
4528
+ required: config.tenant.required,
4529
+ fieldType: config.tenant.fieldType
4505
4530
  }));
4506
4531
  plugins.push(aggregateHelpersPlugin());
4507
4532
  if (config.plugins.length > 0) plugins.push(...config.plugins);
@@ -4703,13 +4728,29 @@ async function runUnit(config, ctx, body) {
4703
4728
  //#endregion
4704
4729
  //#region src/engine/define-crm.ts
4705
4730
  function defineCrm(shape = {}) {
4706
- const tenantFieldType = shape.tenantFieldType ?? "objectId";
4707
- const multiTenant = shape.multiTenant ?? true;
4731
+ /**
4732
+ * The pre-consolidation tenant keys are REFUSED, never absorbed.
4733
+ *
4734
+ * This entry reads `shape.tenant`, so a leftover `multiTenant` is invisible and
4735
+ * the `?? true` default applies repo-core's defaults instead. Both outcomes
4736
+ * return plausible data:
4737
+ * - `multiTenant: false` -> tenancy ON, and the host loses its company-wide reads;
4738
+ * - `multiTenant: { tenantField: 'business' }` -> scoping on `organizationId`,
4739
+ * a field those documents do not carry.
4740
+ *
4741
+ * tsc cannot cover this: excess-property checking does not fire for a typed
4742
+ * variable, a spread, or a shape handed over from a dependency's dist — which is
4743
+ * how every real host composes one. Hence a describe-time check, shared from
4744
+ * repo-core so the nine kernels making this same rename cannot each forget it.
4745
+ */
4746
+ assertNoLegacyTenantKeys(shape, "defineCrm", [["tenantFieldType", "tenant.fieldType"]]);
4747
+ const tenant = resolveTenantConfig(shape.tenant ?? true);
4748
+ if (tenant.strategy === "custom") throw new Error("[crm] tenant.strategy 'custom' is unsupported: CRM repositories require one concrete tenant field");
4708
4749
  const forceRecreate = shape.forceRecreate ?? false;
4709
4750
  const autoIndex = shape.autoIndex ?? true;
4710
4751
  const modules = resolveCrmModules(shape.modules);
4711
4752
  const modelBlueprint = defineCrmModels({
4712
- tenantFieldType,
4753
+ tenant,
4713
4754
  ...shape.modelPrefix !== void 0 ? { modelPrefix: shape.modelPrefix } : {},
4714
4755
  modules,
4715
4756
  forceRecreate,
@@ -4754,9 +4795,8 @@ function defineCrm(shape = {}) {
4754
4795
  if (runtime.outbox && runtime.outbox.transactionalSave !== true) throw new Error("[crm] the configured OutboxStore does not declare `outbox.transactionalSave` — crm events would not commit atomically with the writes that emit them. See OutboxStore.transactionalSave in @classytic/primitives/outbox; arc's `repositoryAsOutboxStore` declares it. This is a property of the STORE, not of the database, and `allowNonTransactional` does not waive it.");
4755
4796
  const resolved = {
4756
4797
  connection,
4757
- tenantFieldType,
4798
+ tenant,
4758
4799
  ...shape.modelPrefix !== void 0 ? { modelPrefix: shape.modelPrefix } : {},
4759
- multiTenant,
4760
4800
  forceRecreate,
4761
4801
  autoIndex,
4762
4802
  modules,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/crm",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
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": "SEE LICENSE IN LICENSE",
@@ -55,18 +55,18 @@
55
55
  },
56
56
  "peerDependencies": {
57
57
  "@classytic/contact": ">=0.1.0",
58
- "@classytic/mongokit": ">=3.32.0",
59
- "@classytic/primitives": ">=0.14.0",
60
- "@classytic/repo-core": ">=0.13.0",
58
+ "@classytic/mongokit": ">=3.33.0",
59
+ "@classytic/primitives": ">=0.23.0",
60
+ "@classytic/repo-core": ">=0.24.0",
61
61
  "mongoose": ">=9.4.1",
62
62
  "zod": ">=4.0.0"
63
63
  },
64
64
  "devDependencies": {
65
65
  "@classytic/contact": ">=0.1.0",
66
66
  "@classytic/dev-tools": "^0.2.0",
67
- "@classytic/mongokit": "^3.25.0",
68
- "@classytic/primitives": ">=0.20.0",
69
- "@classytic/repo-core": ">=0.20.0",
67
+ "@classytic/mongokit": "^3.33.0",
68
+ "@classytic/primitives": ">=0.23.0",
69
+ "@classytic/repo-core": ">=0.24.0",
70
70
  "@types/node": "^25.5.0",
71
71
  "mongodb-memory-server": "^10.1.4",
72
72
  "mongoose": "^9.4.1",