@classytic/party 0.1.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.
@@ -0,0 +1,592 @@
1
+ import { OperationOptions, PaginationConfig, PluginType, Repository } from "@classytic/mongokit";
2
+ import { Connection, Model, Schema, SchemaDefinitionProperty, Types } from "mongoose";
3
+ import { DomainEvent, EventHandler, EventMeta, EventTransport } from "@classytic/primitives/events";
4
+ import { z } from "zod";
5
+ import { OutboxStore } from "@classytic/primitives/outbox";
6
+ import { OperationContext } from "@classytic/primitives/context";
7
+ import { ExternalRef, ExternalRef as ExternalRef$1 } from "@classytic/primitives/reference";
8
+ import { PersonName, PersonName as PersonName$1 } from "@classytic/primitives/person";
9
+ import { ContactAddress } from "@classytic/primitives/address";
10
+ import { HttpError } from "@classytic/repo-core/errors";
11
+ //#region src/domain/enums.d.ts
12
+ /**
13
+ * Party-native enumerations.
14
+ *
15
+ * These are identity-kernel concepts (kind, role, lifecycle) — they have no
16
+ * one-to-one primitive to subset, so they are declared here as the source of
17
+ * truth. Domain FACETS (finance terms, CRM stage, loyalty tier) live in their
18
+ * OWN packages keyed by `partyId`; they are deliberately NOT modelled here —
19
+ * that separation is the whole point of the SAP-style thin identity kernel
20
+ * (vs. Odoo's `res.partner` god-object).
21
+ */
22
+ /** What kind of real-world entity a party is. */
23
+ declare const PARTY_KINDS: readonly ['person', 'company', 'group'];
24
+ type PartyKind = (typeof PARTY_KINDS)[number];
25
+ /**
26
+ * Roles a party plays. NON-exclusive — one party can be a customer AND a
27
+ * supplier AND a contact simultaneously (the exact de-duplication a shared
28
+ * identity buys you). Roles are additive flags, never separate entities.
29
+ */
30
+ declare const PARTY_ROLES: readonly ['customer', 'supplier', 'contact', 'employee', 'partner'];
31
+ type PartyRole = (typeof PARTY_ROLES)[number];
32
+ /**
33
+ * Identity lifecycle. `merged` is terminal — the row survives as a redirect
34
+ * (`mergedInto`) so historical references (orders, invoices) still resolve.
35
+ */
36
+ declare const PARTY_STATUSES: readonly ['active', 'archived', 'merged'];
37
+ type PartyStatus = (typeof PARTY_STATUSES)[number];
38
+ /** Channel kind for a contact point. Drives normalization + dedup keys. */
39
+ declare const CONTACT_POINT_TYPES: readonly ['email', 'phone'];
40
+ type ContactPointType = (typeof CONTACT_POINT_TYPES)[number];
41
+ /**
42
+ * Address role. Mirrors Odoo's `type` (contact/invoice/delivery/other) but
43
+ * as a first-class enum. A party carries many; `isDefault` picks the fallback.
44
+ */
45
+ declare const ADDRESS_TYPES: readonly ['billing', 'shipping', 'contact', 'other'];
46
+ type AddressType = (typeof ADDRESS_TYPES)[number];
47
+ //#endregion
48
+ //#region src/domain/value-objects.d.ts
49
+ /**
50
+ * A postal address on a party. Extends primitives' `ContactAddress` (recipient
51
+ * name/phone/email over the base `Address`) with a role + default flag —
52
+ * mirrors Odoo's per-address `type` without the child-partner overhead.
53
+ */
54
+ interface PartyAddress extends ContactAddress {
55
+ type: AddressType;
56
+ isDefault: boolean;
57
+ /** Human label ("Head office", "Warehouse 2"). */
58
+ label?: string;
59
+ }
60
+ /**
61
+ * A reachable identity channel. `value` is what the user typed; `key` is the
62
+ * normalized dedup form (`email:ada@x.com`, `phone:+8801...`). Structurally a
63
+ * normalized, verifiable superset of primitives' flat `ContactInfo` (§P5) — the
64
+ * kernel dedups on `key` but never mangles the display `value`.
65
+ */
66
+ interface ContactPoint {
67
+ type: ContactPointType;
68
+ value: string;
69
+ /** Normalized dedup key — the `identityKeys` index entry. */
70
+ key: string;
71
+ isPrimary: boolean;
72
+ verified: boolean;
73
+ }
74
+ /**
75
+ * Statutory tax identity. Country-agnostic slots — a BD deployment fills
76
+ * `bin`/`vat`; `fiscalPositionCode` is an opaque host-policy string the active
77
+ * jurisdiction pack interprets. Finance TERMS (AR/AP accounts, credit, payment
78
+ * terms) are NOT here — they are the finance facet keyed by `partyId`, not
79
+ * identity.
80
+ */
81
+ interface TaxIdentity {
82
+ vat?: string;
83
+ /** Business Identification Number (BD) or equivalent national registry id. */
84
+ bin?: string;
85
+ /** Opaque fiscal-position code the active jurisdiction pack resolves. */
86
+ fiscalPositionCode?: string;
87
+ /** ISO 3166-1 alpha-2 tax residence country. */
88
+ countryCode?: string;
89
+ }
90
+ /**
91
+ * The read-model a consuming domain freezes onto its own document (orders,
92
+ * invoices) at write time — the party equivalent of order's `customerSnapshot`.
93
+ * Never a live join; capture-at-time so a later merge/rename can't rewrite
94
+ * history.
95
+ */
96
+ interface PartySnapshot {
97
+ partyId: string;
98
+ kind: PartyKind;
99
+ displayName: string;
100
+ legalName?: string;
101
+ primaryEmail?: string;
102
+ primaryPhone?: string;
103
+ roles: PartyRole[];
104
+ status: PartyStatus;
105
+ taxIdentity?: TaxIdentity;
106
+ }
107
+ //#endregion
108
+ //#region src/domain/ports.d.ts
109
+ /**
110
+ * Operation context threaded through every domain verb. Extends primitives'
111
+ * `OperationContext`; narrows the id fields to what Mongoose stores cleanly.
112
+ */
113
+ interface PartyContext extends OperationContext {
114
+ organizationId?: string | Types.ObjectId;
115
+ actorId?: string | Types.ObjectId;
116
+ /** Mongo transaction session, passed straight to mongokit. */
117
+ session?: unknown;
118
+ /** Cooperative cancellation — threaded to mongokit at choke points. */
119
+ signal?: AbortSignal;
120
+ /** Retry policy for driver round-trips (mongokit `RetryPolicy`). */
121
+ retryPolicy?: unknown;
122
+ }
123
+ /**
124
+ * Identity signals a consumer already knows about a party. `resolveOrCreate`
125
+ * matches on ANY of the normalized keys derived from `emails`/`phones`/`vat`/
126
+ * `bin`/`externalRefs` and falls back to creating a fresh party from the
127
+ * display fields. At least one identity signal OR `displayName` is required.
128
+ */
129
+ interface ResolvePartyInput {
130
+ kind?: PartyKind;
131
+ displayName?: string;
132
+ legalName?: string;
133
+ /** Structured name for `kind: 'person'` — primitives' `PersonName` (§P3). */
134
+ personName?: PersonName$1;
135
+ emails?: string[];
136
+ phones?: string[];
137
+ addresses?: PartyAddress[];
138
+ taxIdentity?: TaxIdentity;
139
+ /** Roles to ensure on the resolved/created party (additive, deduped). */
140
+ roles?: PartyRole[];
141
+ /** External-store links to attach (migration seam) — primitives' `ExternalRef`. */
142
+ externalRefs?: ExternalRef$1[];
143
+ /** Auth user id to link (1:many — a company party has many portal users). */
144
+ userId?: string;
145
+ parentId?: string;
146
+ metadata?: Record<string, unknown>;
147
+ }
148
+ /** Result of a resolve — the party plus whether this call created it. */
149
+ interface ResolveResult<TParty> {
150
+ party: TParty;
151
+ created: boolean;
152
+ }
153
+ /**
154
+ * The reusable identity resolver. Domains depend on THIS, not on the party
155
+ * models — so the kernel can evolve its storage without touching consumers, and
156
+ * a non-Mongo host could implement the same port over a different backend.
157
+ *
158
+ * The kernel's `PartyRepository` implements it; hosts wire it into order / crm /
159
+ * ledger / commerce as the single identity source.
160
+ */
161
+ interface PartyPort<TParty = unknown> {
162
+ /** Find an existing party by any identity signal, else create one. Race-safe. */
163
+ resolveOrCreate(input: ResolvePartyInput, ctx: PartyContext): Promise<ResolveResult<TParty>>;
164
+ /** Batch identity resolution — avoids N+1 when enriching a list. */
165
+ resolveMany(inputs: ResolvePartyInput[], ctx: PartyContext): Promise<Array<ResolveResult<TParty>>>;
166
+ /** The capture-at-time read model to freeze onto a consuming document. */
167
+ snapshot(partyId: string, ctx: PartyContext): Promise<PartySnapshot>;
168
+ }
169
+ /** Minimal logger surface the in-process bus + repositories log through. */
170
+ interface PartyLogger {
171
+ info?(msg: string, meta?: Record<string, unknown>): void;
172
+ warn?(msg: string, meta?: Record<string, unknown>): void;
173
+ error?(msg: string, meta?: Record<string, unknown>): void;
174
+ debug?(msg: string, meta?: Record<string, unknown>): void;
175
+ }
176
+ //#endregion
177
+ //#region src/models/tenant-field.d.ts
178
+ type TenantFieldType = 'objectId' | 'string';
179
+ interface SchemaBuildConfig {
180
+ /** Schema type for the tenant column. Engine plumbs this everywhere. */
181
+ tenantFieldType: TenantFieldType;
182
+ /**
183
+ * When `false`, models carry NO tenant column (company-wide identity). The
184
+ * engine wires no `multiTenantPlugin` either. Default `true`.
185
+ */
186
+ multiTenant: boolean;
187
+ }
188
+ /**
189
+ * Tenant-column schema definition, or `undefined` when the engine is
190
+ * company-wide (`multiTenant: false`) so the field is simply not added.
191
+ */
192
+ declare function tenantFieldDef(config: SchemaBuildConfig): SchemaDefinitionProperty | undefined;
193
+ //#endregion
194
+ //#region src/models/party.model.d.ts
195
+ interface IPartyDocument {
196
+ _id: Types.ObjectId;
197
+ /** Present only when the engine runs multi-tenant; absent for company-wide identity. */
198
+ organizationId?: Types.ObjectId | string;
199
+ kind: PartyKind;
200
+ displayName: string;
201
+ legalName?: string;
202
+ personName?: PersonName$1;
203
+ contactPoints: ContactPoint[];
204
+ /** Normalized dedup keys derived from contactPoints + taxIdentity — the resolve index. */
205
+ identityKeys: string[];
206
+ addresses: PartyAddress[];
207
+ taxIdentity?: TaxIdentity;
208
+ roles: PartyRole[];
209
+ externalRefs: ExternalRef$1[];
210
+ /** Auth user ids linked to this party (1:many — a company has many portal logins). */
211
+ userLinks: string[];
212
+ parentId?: Types.ObjectId | string;
213
+ status: PartyStatus;
214
+ /** Set when `status: 'merged'` — the survivor this party redirects to. */
215
+ mergedInto?: Types.ObjectId | string;
216
+ tags: string[];
217
+ metadata?: Record<string, unknown>;
218
+ /** Optimistic-concurrency counter (drives `claimVersion`). */
219
+ version: number;
220
+ createdAt: Date;
221
+ updatedAt: Date;
222
+ }
223
+ declare function buildPartySchema(config: SchemaBuildConfig): Schema<IPartyDocument>;
224
+ //#endregion
225
+ //#region src/models/create-models.d.ts
226
+ interface PartyModels {
227
+ Party: Model<IPartyDocument>;
228
+ }
229
+ /** Mongoose model names this package registers — drives the §21 collision check. */
230
+ declare const PARTY_MODEL_NAMES: readonly ['Party'];
231
+ interface CreateModelsOptions extends SchemaBuildConfig {
232
+ /** On a name collision: `false` (default) throws; `true` deletes + re-registers (tests). */
233
+ forceRecreate?: boolean;
234
+ /** Mongoose auto-index builds. Default `true` (dev); `false` in prod (§35). */
235
+ autoIndex?: boolean;
236
+ }
237
+ declare function createPartyModels(connection: Connection, config: CreateModelsOptions): PartyModels;
238
+ //#endregion
239
+ //#region src/events/dispatch.d.ts
240
+ /** Context key carrying a post-commit publish queue (attached by `withTransaction`). */
241
+ declare const PENDING_EVENTS: unique symbol;
242
+ interface PartyEventDispatcherOptions {
243
+ transport: EventTransport;
244
+ outbox?: OutboxStore | undefined;
245
+ logger?: PartyLogger | undefined;
246
+ }
247
+ declare class PartyEventDispatcher {
248
+ readonly name: string;
249
+ private readonly transport;
250
+ private readonly outbox;
251
+ private readonly logger;
252
+ constructor(opts: PartyEventDispatcherOptions);
253
+ /** Raw publish — hosts and the unit-of-work flush use this. */
254
+ publish(event: DomainEvent): Promise<void>;
255
+ subscribe(pattern: string, handler: EventHandler): Promise<() => void>;
256
+ /**
257
+ * Route a domain event with full §P8 discipline. Reads the pending queue and
258
+ * session off `ctx`.
259
+ */
260
+ dispatch(event: DomainEvent, ctx: PartyContext): Promise<void>;
261
+ close(): Promise<void>;
262
+ }
263
+ /**
264
+ * Drain a pending-events queue to the transport AFTER the owning transaction
265
+ * commits. Per-event failures are swallowed — durable delivery is the outbox's
266
+ * job (PACKAGE_RULES §P8.1).
267
+ */
268
+ declare function flushPending(dispatcher: PartyEventDispatcher, queue: DomainEvent[], logger?: PartyLogger): Promise<void>;
269
+ //#endregion
270
+ //#region src/repositories/party.repository.d.ts
271
+ interface PartyRepositoryDeps {
272
+ events: PartyEventDispatcher;
273
+ logger?: PartyLogger | undefined;
274
+ }
275
+ declare class PartyRepository extends Repository<IPartyDocument> implements PartyPort<IPartyDocument> {
276
+ private readonly events;
277
+ constructor(model: Model<IPartyDocument>, deps: PartyRepositoryDeps, plugins?: PluginType[]);
278
+ /** `update` that never returns null — the party was loaded first; a null means a concurrent delete. */
279
+ private mustUpdate;
280
+ resolveOrCreate(input: ResolvePartyInput, ctx: PartyContext): Promise<ResolveResult<IPartyDocument>>;
281
+ resolveMany(inputs: ResolvePartyInput[], ctx: PartyContext): Promise<Array<ResolveResult<IPartyDocument>>>;
282
+ snapshot(partyId: string, ctx: PartyContext): Promise<PartySnapshot>;
283
+ /**
284
+ * Sync identity onto a party — the canonical "identity changed" verb. The
285
+ * party OWNS identity mutation (name, contact points, addresses, tax), so
286
+ * hosts push a facet's current identity here instead of editing it locally.
287
+ * NEW contact points fold in (deduped, deriving their keys + extending
288
+ * `identityKeys`); existing primaries are preserved. Idempotent — syncing the
289
+ * same identity is a no-op. Emits `party:party.updated`.
290
+ */
291
+ syncIdentity(id: string, input: ResolvePartyInput, ctx: PartyContext): Promise<IPartyDocument>;
292
+ addRole(id: string, role: PartyRole, ctx: PartyContext): Promise<IPartyDocument>;
293
+ removeRole(id: string, role: PartyRole, ctx: PartyContext): Promise<IPartyDocument>;
294
+ linkUser(id: string, userId: string, ctx: PartyContext): Promise<IPartyDocument>;
295
+ attachExternalRef(id: string, ref: ExternalRef, ctx: PartyContext): Promise<IPartyDocument>;
296
+ archive(id: string, ctx: PartyContext): Promise<IPartyDocument>;
297
+ restore(id: string, ctx: PartyContext): Promise<IPartyDocument>;
298
+ /**
299
+ * Merge `sourceId` into `targetId`: the survivor absorbs the loser's identity
300
+ * keys, contact points, addresses, roles, external refs, and user links; the
301
+ * loser becomes a terminal `merged` redirect (`mergedInto`). Two-document
302
+ * write → runs in a transaction (falls back to sequential on standalone Mongo).
303
+ */
304
+ merge(sourceId: string, targetId: string, ctx: PartyContext): Promise<IPartyDocument>;
305
+ /** Load a party by ObjectId hex, or throw the typed not-found (§P9-style). */
306
+ loadParty(id: string, ctx: PartyContext): Promise<IPartyDocument>;
307
+ /** Add any new roles/refs/user/keys from `input` onto an already-resolved party. */
308
+ private enrich;
309
+ /** Route a domain event with meta stamped from ctx + the party's org. */
310
+ private dispatch;
311
+ }
312
+ //#endregion
313
+ //#region src/engine/engine-types.d.ts
314
+ interface PartyConfig {
315
+ /** Mongoose connection — the package creates its model on this connection. */
316
+ connection: Connection;
317
+ /**
318
+ * Multi-tenant scoping. Identity is FREQUENTLY company-wide (one customer is
319
+ * the same person in every branch) — such hosts pass `false` and the model
320
+ * carries no tenant column. SaaS / true-isolation hosts pass `true` (default).
321
+ */
322
+ multiTenant?: boolean;
323
+ /**
324
+ * Tenant column shape when `multiTenant` is on. `'objectId'` (default)
325
+ * matches Better Auth orgs (unlocks `$lookup`/`.populate()`); `'string'`
326
+ * matches UUID/slug hosts. Threaded into schema + plugin (§9.2).
327
+ */
328
+ tenantFieldType?: TenantFieldType;
329
+ /** Arc-compatible event transport. Default: in-process bus. */
330
+ eventTransport?: EventTransport;
331
+ /** Transactional outbox (§P8). When wired, events persist in the write's session. */
332
+ outbox?: OutboxStore;
333
+ /** Logger surface for the in-process bus + repository. */
334
+ logger?: PartyLogger;
335
+ /** Pagination defaults for the repository (mongokit). */
336
+ pagination?: PaginationConfig;
337
+ /** Extra mongokit plugins applied after the defaults. */
338
+ plugins?: PluginType[];
339
+ /** On a model-name collision: `false` (default) throws; `true` re-registers (tests, §21). */
340
+ forceRecreate?: boolean;
341
+ /** Mongoose auto-index builds. Default `true` (dev); `false` in prod (§35). */
342
+ autoIndex?: boolean;
343
+ /** Downgrade the boot `transactions` capability requirement to a warning (standalone Mongo). */
344
+ allowNonTransactional?: boolean;
345
+ }
346
+ interface ResolvedPartyConfig {
347
+ connection: Connection;
348
+ multiTenant: boolean;
349
+ tenantFieldType: TenantFieldType;
350
+ forceRecreate: boolean;
351
+ autoIndex: boolean;
352
+ allowNonTransactional: boolean;
353
+ logger?: PartyLogger;
354
+ pagination?: PaginationConfig;
355
+ plugins: PluginType[];
356
+ }
357
+ interface PartyRepositories {
358
+ party: PartyRepository;
359
+ }
360
+ interface PartyEngine {
361
+ /** Repositories ARE the API surface (§1). */
362
+ readonly repositories: Readonly<PartyRepositories>;
363
+ /**
364
+ * The reusable identity contract other domains depend on (order / crm /
365
+ * ledger / commerce resolve identity through THIS, not the models). It IS the
366
+ * party repository, narrowed to the port — swap the storage without touching
367
+ * a single consumer.
368
+ */
369
+ readonly port: PartyPort<IPartyDocument>;
370
+ /** Arc-compatible event transport (a dispatcher wrapping the configured one). */
371
+ readonly events: PartyEventDispatcher;
372
+ /** Mongoose models — for arc adapter wiring. */
373
+ readonly models: Readonly<PartyModels>;
374
+ /** Resolved configuration (frozen). */
375
+ readonly config: Readonly<ResolvedPartyConfig>;
376
+ /**
377
+ * Run `body` inside a transaction with post-commit event discipline (§P8.1):
378
+ * events dispatched through `txCtx` flush to the transport only after commit.
379
+ */
380
+ withTransaction<T>(ctx: PartyContext, body: (txCtx: PartyContext) => Promise<T>): Promise<T>;
381
+ /** Build/align indexes. Call once per deploy when booted `autoIndex: false` (§35). */
382
+ syncIndexes(): Promise<void>;
383
+ /** Release resources. Idempotent. */
384
+ dispose(): Promise<void>;
385
+ }
386
+ //#endregion
387
+ //#region src/engine/create-party.d.ts
388
+ declare function createParty(config: PartyConfig): Promise<PartyEngine>;
389
+ //#endregion
390
+ //#region src/repositories/repo-options.d.ts
391
+ declare function repoOptionsFromCtx(ctx: PartyContext): OperationOptions;
392
+ //#endregion
393
+ //#region src/domain/state-machine.d.ts
394
+ /**
395
+ * Party identity lifecycle FSM — the compile-time twin of the `repo.claim()`
396
+ * CAS the repository uses for status transitions (PACKAGE_RULES §Concurrency).
397
+ *
398
+ * active ──archive──▶ archived ──restore──▶ active
399
+ * active ──merge────▶ merged (terminal; row survives as a `mergedInto` redirect)
400
+ *
401
+ * `merged` is terminal: a merged party is never resurrected — historical
402
+ * references resolve THROUGH it to the survivor, so the identity graph stays
403
+ * append-only and auditable.
404
+ */
405
+ declare const PARTY_STATE_MACHINE: import("@classytic/primitives/state-machine").StateMachine<"active" | "archived" | "merged">;
406
+ //#endregion
407
+ //#region src/domain/identity.d.ts
408
+ /** De-duplicate an array preserving first-seen order. */
409
+ declare function dedupe<T>(arr: readonly T[]): T[];
410
+ /**
411
+ * Turn the input's `emails[]` / `phones[]` into normalized `ContactPoint`s. The
412
+ * first email and first phone are marked primary; blanks are skipped.
413
+ */
414
+ declare function buildContactPoints(input: ResolvePartyInput): ContactPoint[];
415
+ /**
416
+ * The de-dup keys a party is resolved on: normalized emails/phones, VAT/BIN
417
+ * statutory ids, and external-store refs. Blank/degenerate keys are dropped so
418
+ * they never poison the unique index.
419
+ */
420
+ declare function deriveIdentityKeys(contactPoints: readonly ContactPoint[], input: ResolvePartyInput): string[];
421
+ /** Best display name from the available signals — never empty. */
422
+ declare function resolveDisplayName(input: ResolvePartyInput, contactPoints: readonly ContactPoint[]): string;
423
+ /** The primary (or first) contact-point value of a channel type. */
424
+ declare function pickPrimary(contactPoints: readonly ContactPoint[], type: 'email' | 'phone'): string | undefined;
425
+ //#endregion
426
+ //#region src/domain/merge.d.ts
427
+ /** Union two string arrays, first-seen order, no duplicates. */
428
+ declare function unionStrings(a: readonly string[], b: readonly string[]): string[];
429
+ /** Union external refs by `(sourceModel, sourceId)` identity. */
430
+ declare function unionRefs(a: readonly ExternalRef[], b: readonly ExternalRef[]): ExternalRef[];
431
+ /**
432
+ * Union contact points by their normalized `key`. The survivor keeps its own
433
+ * primaries; absorbed points come in as non-primary (a merge must never create
434
+ * two primaries of the same channel).
435
+ */
436
+ declare function unionContacts(a: readonly ContactPoint[], b: readonly ContactPoint[]): ContactPoint[];
437
+ //#endregion
438
+ //#region src/domain/normalize.d.ts
439
+ /** Lowercase + trim an email. Conservative — never strips dots/plus tags. */
440
+ declare function normalizeEmail(raw: string): string;
441
+ /**
442
+ * Canonical phone form for dedup. Uses primitives' `parsePhone`: a valid E.164
443
+ * input collapses to its canonical `+<cc><national>`; anything the strict parser
444
+ * rejects (local formats without a country code) falls back to a digit-only
445
+ * key so at least same-format duplicates still collapse. Hosts wanting global
446
+ * dedup pass already-E.164 numbers.
447
+ */
448
+ declare function normalizePhone(raw: string): string;
449
+ /** The stable dedup key for a contact point (`email:ada@x.com`, `phone:+8801...`). */
450
+ declare function contactPointKey(type: ContactPointType, value: string): string;
451
+ /** A tax-identity dedup key (`vat:...` / `bin:...`) — cross-scope-stable statutory id. */
452
+ declare function taxIdentityKey(kind: 'vat' | 'bin', value: string): string;
453
+ //#endregion
454
+ //#region src/domain/errors.d.ts
455
+ /** Base class so hosts can `instanceof PartyError` across every party throwable. */
456
+ declare class PartyError extends Error implements HttpError {
457
+ readonly status: number;
458
+ readonly code: string;
459
+ readonly meta?: Record<string, unknown>;
460
+ constructor(status: number, code: string, message: string, meta?: Record<string, unknown>);
461
+ }
462
+ /** A referenced party id does not exist (or is out of the caller's scope). */
463
+ declare class PartyNotFoundError extends PartyError {
464
+ constructor(id: string);
465
+ }
466
+ /**
467
+ * A merge was attempted against an illegal target — self-merge, merging into an
468
+ * already-merged (redirected) party, or a cross-scope pair. Carries both ids.
469
+ */
470
+ declare class PartyMergeError extends PartyError {
471
+ constructor(message: string, sourceId: string, targetId: string);
472
+ }
473
+ /**
474
+ * `resolveOrCreate` lost the create race and the winning row could not be
475
+ * re-read (should be transient — surfaced rather than swallowed).
476
+ */
477
+ declare class PartyResolveConflictError extends PartyError {
478
+ constructor(identityKey: string);
479
+ }
480
+ /**
481
+ * Optimistic-concurrency guard failed — the party was modified by another
482
+ * writer between read and write. Callers re-fetch and retry.
483
+ */
484
+ declare class PartyVersionConflictError extends PartyError {
485
+ constructor(id: string, expected: number);
486
+ }
487
+ /**
488
+ * A Mongoose model name this package registers is already on the connection.
489
+ * Default init throws this; `forceRecreate: true` opts into delete+re-register
490
+ * (test fixtures / hot reload only). PACKAGE_RULES §21.
491
+ */
492
+ declare class PartyModelCollisionError extends PartyError {
493
+ constructor(modelName: string);
494
+ }
495
+ /** Input failed a domain-verb precondition (before any DB write). */
496
+ declare class PartyValidationError extends PartyError {
497
+ constructor(message: string, meta?: Record<string, unknown>);
498
+ }
499
+ //#endregion
500
+ //#region src/events/event-constants.d.ts
501
+ /** PACKAGE_RULES §12: `package:resource.verb`. Emit ONLY these catalog names. */
502
+ declare const PARTY_EVENTS: {
503
+ readonly CREATED: 'party:party.created';
504
+ readonly UPDATED: 'party:party.updated';
505
+ readonly ROLE_ADDED: 'party:party.role_added';
506
+ readonly ROLE_REMOVED: 'party:party.role_removed';
507
+ readonly USER_LINKED: 'party:party.user_linked';
508
+ readonly EXTERNAL_REF_ATTACHED: 'party:party.external_ref_attached';
509
+ readonly ARCHIVED: 'party:party.archived';
510
+ readonly RESTORED: 'party:party.restored';
511
+ readonly MERGED: 'party:party.merged';
512
+ };
513
+ type PartyEventName = (typeof PARTY_EVENTS)[keyof typeof PARTY_EVENTS];
514
+ //#endregion
515
+ //#region src/events/event-payloads.d.ts
516
+ interface PartyCreatedPayload {
517
+ partyId: string;
518
+ kind: PartyKind;
519
+ displayName: string;
520
+ roles: PartyRole[];
521
+ organizationId?: string;
522
+ }
523
+ interface PartyUpdatedPayload {
524
+ partyId: string;
525
+ /** Top-level fields that changed on this write. */
526
+ changed: string[];
527
+ }
528
+ interface PartyRoleChangedPayload {
529
+ partyId: string;
530
+ role: PartyRole;
531
+ }
532
+ interface PartyUserLinkedPayload {
533
+ partyId: string;
534
+ userId: string;
535
+ }
536
+ interface PartyExternalRefAttachedPayload {
537
+ partyId: string;
538
+ sourceModel: string;
539
+ sourceId: string;
540
+ }
541
+ interface PartyLifecyclePayload {
542
+ partyId: string;
543
+ }
544
+ interface PartyMergedPayload {
545
+ /** The party that was merged away (now a redirect). */
546
+ sourceId: string;
547
+ /** The surviving party. */
548
+ targetId: string;
549
+ /** How many identity keys the survivor absorbed. */
550
+ absorbedKeys: number;
551
+ }
552
+ /** Name → payload map — subscribers get exact typing from the event name. */
553
+ interface PartyEventPayloads {
554
+ [PARTY_EVENTS.CREATED]: PartyCreatedPayload;
555
+ [PARTY_EVENTS.UPDATED]: PartyUpdatedPayload;
556
+ [PARTY_EVENTS.ROLE_ADDED]: PartyRoleChangedPayload;
557
+ [PARTY_EVENTS.ROLE_REMOVED]: PartyRoleChangedPayload;
558
+ [PARTY_EVENTS.USER_LINKED]: PartyUserLinkedPayload;
559
+ [PARTY_EVENTS.EXTERNAL_REF_ATTACHED]: PartyExternalRefAttachedPayload;
560
+ [PARTY_EVENTS.ARCHIVED]: PartyLifecyclePayload;
561
+ [PARTY_EVENTS.RESTORED]: PartyLifecyclePayload;
562
+ [PARTY_EVENTS.MERGED]: PartyMergedPayload;
563
+ }
564
+ //#endregion
565
+ //#region src/events/event-catalog.d.ts
566
+ interface PartyEventDefinition<TSchema extends z.ZodType = z.ZodType> {
567
+ name: string;
568
+ version: number;
569
+ description: string;
570
+ zodSchema: TSchema;
571
+ schema: Record<string, unknown>;
572
+ create(payload: z.infer<TSchema>, meta?: Partial<EventMeta>): DomainEvent<z.infer<TSchema>>;
573
+ }
574
+ declare const partyEventDefinitions: ReadonlyArray<PartyEventDefinition>;
575
+ //#endregion
576
+ //#region src/events/helpers.d.ts
577
+ declare function createEvent<T>(type: string, payload: T, ctx?: PartyContext, meta?: Partial<EventMeta>): DomainEvent<T>;
578
+ //#endregion
579
+ //#region src/events/in-process-bus.d.ts
580
+ declare class InProcessPartyBus implements EventTransport {
581
+ readonly name = "in-process-party";
582
+ private subs;
583
+ private readonly logger;
584
+ constructor(options?: {
585
+ logger?: PartyLogger | undefined;
586
+ });
587
+ publish(event: DomainEvent): Promise<void>;
588
+ subscribe(pattern: string, handler: EventHandler): Promise<() => void>;
589
+ close(): Promise<void>;
590
+ }
591
+ //#endregion
592
+ export { ADDRESS_TYPES, type AddressType, CONTACT_POINT_TYPES, type ContactPoint, type ContactPointType, type ExternalRef, type IPartyDocument, InProcessPartyBus, PARTY_EVENTS, PARTY_KINDS, PARTY_MODEL_NAMES, PARTY_ROLES, PARTY_STATE_MACHINE, PARTY_STATUSES, PENDING_EVENTS, type PartyAddress, type PartyConfig, type PartyContext, type PartyEngine, PartyError, PartyEventDispatcher, type PartyEventName, type PartyEventPayloads, type PartyKind, type PartyLogger, PartyMergeError, PartyModelCollisionError, type PartyModels, PartyNotFoundError, type PartyPort, type PartyRepositories, PartyRepository, type PartyRepositoryDeps, PartyResolveConflictError, type PartyRole, type PartySnapshot, type PartyStatus, PartyValidationError, PartyVersionConflictError, type PersonName, type ResolvePartyInput, type ResolveResult, type ResolvedPartyConfig, type SchemaBuildConfig, type TaxIdentity, type TenantFieldType, buildContactPoints, buildPartySchema, contactPointKey, createEvent, createParty, createParty as default, createPartyModels, dedupe, deriveIdentityKeys, flushPending, normalizeEmail, normalizePhone, partyEventDefinitions, pickPrimary, repoOptionsFromCtx, resolveDisplayName, taxIdentityKey, tenantFieldDef, unionContacts, unionRefs, unionStrings };