@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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,31 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@classytic/party` are documented here. Adheres to
4
+ [Keep a Changelog](https://keepachangelog.com/) and [SemVer](https://semver.org/).
5
+
6
+ ## [0.1.0] - 2026-07-13
7
+
8
+ Initial release — the Party / Business-Partner identity kernel.
9
+
10
+ - **`createParty({ connection, multiTenant?, … })`** engine factory; owns one
11
+ `Party` Mongoose model, an event dispatcher, and a `PartyPort`.
12
+ - **`Party` model** — kind (person/company/group), non-exclusive roles,
13
+ contact points (normalized `identityKeys`), addresses, tax identity, B2B
14
+ `parentId`, 1:many `userLinks`, `externalRefs`, lifecycle (active/archived/merged).
15
+ - **`resolveOrCreate`** — race-safe dedup by email/phone/vat/bin/externalRef;
16
+ unique multikey `identityKeys` index is the authoritative guard.
17
+ - **`merge`** — non-lossy survivor absorption + terminal redirect; `snapshot`
18
+ follows the redirect. Runs in a transaction.
19
+ - **Roles** — `addRole` / `removeRole`; **lifecycle** — `archive` / `restore`
20
+ via a `defineStateMachine` + `claim` CAS.
21
+ - **`syncIdentity(id, input)`** — the canonical identity-mutation verb: merges
22
+ new contact points (deriving keys, extending `identityKeys`), updates
23
+ name/addresses/tax, emits `party:party.updated`. Hosts push a facet's current
24
+ identity here so the party stays the source of truth (idempotent).
25
+ - **`PartyPort`** — the reusable identity contract for consuming domains.
26
+ - **Events** — `party:party.*` with a Zod source catalog + no-drift test; P8.1
27
+ unit-of-work (`withTransaction` + `PENDING_EVENTS` + `flushPending`).
28
+ - Thin identity (SAP Business-Partner shape) — domain facets stay in their own
29
+ packages keyed by `partyId`. Primitives reused for Address / PersonName /
30
+ ExternalRef / phone parsing. 28 tests (18 unit + 10 integration).
31
+ - Toolchain: TypeScript 7, tsdown 0.22, ESM, `exactOptionalPropertyTypes`.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Classytic
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,113 @@
1
+ # @classytic/party
2
+
3
+ **The Party / Business-Partner identity kernel for MongoDB.** One canonical
4
+ identity per real-world person, company, or group — carrying non-exclusive roles
5
+ (customer / supplier / contact / employee / partner), contact points, addresses,
6
+ tax identity, B2B hierarchy, and 1:many auth-user links. With race-safe
7
+ de-duplication, merge, and a reusable resolver port.
8
+
9
+ Built on Mongoose + [`@classytic/mongokit`](https://npmjs.com/package/@classytic/mongokit)
10
+ + [`@classytic/primitives`](https://npmjs.com/package/@classytic/primitives) +
11
+ [`@classytic/repo-core`](https://npmjs.com/package/@classytic/repo-core). Any
12
+ commerce / ERP / SaaS app on MongoDB can adopt it.
13
+
14
+ ## Why
15
+
16
+ Most stacks model "the customer" in several places — a storefront customer, a
17
+ CRM contact, an accounting partner, a payout recipient — none sharing an
18
+ identity. That fragmentation is where "is this the same person?" bugs live.
19
+
20
+ `party` is the **single identity spine**. It follows the **SAP Business Partner**
21
+ shape: a *thin* identity that carries roles, and lets each domain keep its own
22
+ **facet** keyed by `partyId`. It is deliberately **not** an Odoo `res.partner`
23
+ god-object — no accounting accounts, credit limits, loyalty tiers, KYC, or
24
+ payout methods live on the party. Those stay in `@classytic/ledger`,
25
+ `@classytic/loyalty`, `@classytic/payee`, `@classytic/crm`, … and point at a
26
+ `partyId`.
27
+
28
+ ```
29
+ @classytic/party (WHO) ← one deduped identity + roles
30
+ ├── ledger finance facet (AR/AP, credit, terms) → partyId
31
+ ├── crm funnel facet (stage, campaigns) → partyId
32
+ ├── loyalty rewards facet (points, tier) → partyId
33
+ ├── payee payout facet (methods, KYC) → partyId
34
+ ├── commission earnings facet (plans, records) → partyId
35
+ └── referral attribution (who referred whom) → partyId
36
+ ```
37
+
38
+ ## Install
39
+
40
+ ```bash
41
+ npm i @classytic/party
42
+ ```
43
+
44
+ Peers: `@classytic/mongokit >=3.20`, `@classytic/primitives >=0.9.1`,
45
+ `@classytic/repo-core >=0.9`, `mongoose >=9.4.1`, `zod >=4`.
46
+
47
+ ## Quick start
48
+
49
+ ```ts
50
+ import mongoose from 'mongoose';
51
+ import { createParty } from '@classytic/party';
52
+
53
+ const party = await createParty({
54
+ connection: mongoose.connection,
55
+ multiTenant: false, // identity is company-wide here; pass true for per-tenant SaaS
56
+ });
57
+
58
+ // Resolve-or-create by ANY identity signal — race-safe, dedup by email/phone/vat.
59
+ const { party: ada, created } = await party.port.resolveOrCreate(
60
+ {
61
+ displayName: 'Ada Lovelace',
62
+ emails: ['ada@example.com'],
63
+ phones: ['+8801712345678'],
64
+ roles: ['customer'],
65
+ },
66
+ {},
67
+ );
68
+
69
+ // Same person, different casing + a role added → SAME party, no duplicate.
70
+ await party.port.resolveOrCreate({ emails: ['ADA@example.com'], roles: ['supplier'] }, {});
71
+
72
+ // Freeze a snapshot onto an order/invoice (never a live join).
73
+ const snap = await party.port.snapshot(String(ada._id), {});
74
+
75
+ // Merge a duplicate — survivor absorbs identity; the loser becomes a redirect.
76
+ await party.repositories.party.merge(dupId, survivorId, {});
77
+ ```
78
+
79
+ ## The `PartyPort` — the reusable contract
80
+
81
+ Consuming domains depend on `engine.port` (a `PartyPort`), not the models:
82
+
83
+ ```ts
84
+ interface PartyPort<TParty> {
85
+ resolveOrCreate(input, ctx): Promise<{ party: TParty; created: boolean }>;
86
+ resolveMany(inputs, ctx): Promise<Array<{ party: TParty; created: boolean }>>;
87
+ snapshot(partyId, ctx): Promise<PartySnapshot>; // follows merge redirects
88
+ }
89
+ ```
90
+
91
+ Wire it into order / crm / ledger as the single identity source. Swap the
92
+ kernel's storage without touching a consumer.
93
+
94
+ ## What it guarantees
95
+
96
+ - **Race-safe dedup** — a unique multikey index on `identityKeys` is the
97
+ authoritative guard; concurrent creates collapse to one party.
98
+ - **Non-lossy merge** — the survivor absorbs the loser's keys, roles, contacts,
99
+ refs, and user links; the loser stays as a terminal `merged` redirect, so
100
+ historical references (and `snapshot`) still resolve.
101
+ - **Arc-native errors** — every throwable implements repo-core's `HttpError`.
102
+ - **Event catalog** — `party:party.*` events with a Zod source catalog for arc's
103
+ `EventRegistry` (validation + OpenAPI). Bring any arc `EventTransport`.
104
+
105
+ ## Roles are flags, never tables
106
+
107
+ A party with `roles: ['customer', 'supplier']` is simultaneously both. Grant with
108
+ `addRole`, revoke with `removeRole`. No separate customer/vendor/contact
109
+ entities — that's the duplication this kernel exists to remove.
110
+
111
+ ## License
112
+
113
+ MIT
@@ -0,0 +1,238 @@
1
+ import { z } from "zod";
2
+ import { DomainEvent, EventHandler, EventMeta, EventTransport } from "@classytic/primitives/events";
3
+ import { Types } from "mongoose";
4
+ import { OperationContext } from "@classytic/primitives/context";
5
+ import "@classytic/primitives/reference";
6
+ import "@classytic/primitives/person";
7
+ import "@classytic/primitives/address";
8
+ import { OutboxStore } from "@classytic/primitives/outbox";
9
+ //#region src/events/event-constants.d.ts
10
+ /** PACKAGE_RULES §12: `package:resource.verb`. Emit ONLY these catalog names. */
11
+ declare const PARTY_EVENTS: {
12
+ readonly CREATED: 'party:party.created';
13
+ readonly UPDATED: 'party:party.updated';
14
+ readonly ROLE_ADDED: 'party:party.role_added';
15
+ readonly ROLE_REMOVED: 'party:party.role_removed';
16
+ readonly USER_LINKED: 'party:party.user_linked';
17
+ readonly EXTERNAL_REF_ATTACHED: 'party:party.external_ref_attached';
18
+ readonly ARCHIVED: 'party:party.archived';
19
+ readonly RESTORED: 'party:party.restored';
20
+ readonly MERGED: 'party:party.merged';
21
+ };
22
+ type PartyEventName = (typeof PARTY_EVENTS)[keyof typeof PARTY_EVENTS];
23
+ //#endregion
24
+ //#region src/domain/enums.d.ts
25
+ /**
26
+ * Party-native enumerations.
27
+ *
28
+ * These are identity-kernel concepts (kind, role, lifecycle) — they have no
29
+ * one-to-one primitive to subset, so they are declared here as the source of
30
+ * truth. Domain FACETS (finance terms, CRM stage, loyalty tier) live in their
31
+ * OWN packages keyed by `partyId`; they are deliberately NOT modelled here —
32
+ * that separation is the whole point of the SAP-style thin identity kernel
33
+ * (vs. Odoo's `res.partner` god-object).
34
+ */
35
+ /** What kind of real-world entity a party is. */
36
+ declare const PARTY_KINDS: readonly ['person', 'company', 'group'];
37
+ type PartyKind = (typeof PARTY_KINDS)[number];
38
+ /**
39
+ * Roles a party plays. NON-exclusive — one party can be a customer AND a
40
+ * supplier AND a contact simultaneously (the exact de-duplication a shared
41
+ * identity buys you). Roles are additive flags, never separate entities.
42
+ */
43
+ declare const PARTY_ROLES: readonly ['customer', 'supplier', 'contact', 'employee', 'partner'];
44
+ type PartyRole = (typeof PARTY_ROLES)[number];
45
+ //#endregion
46
+ //#region src/events/event-payloads.d.ts
47
+ interface PartyCreatedPayload {
48
+ partyId: string;
49
+ kind: PartyKind;
50
+ displayName: string;
51
+ roles: PartyRole[];
52
+ organizationId?: string;
53
+ }
54
+ interface PartyUpdatedPayload {
55
+ partyId: string;
56
+ /** Top-level fields that changed on this write. */
57
+ changed: string[];
58
+ }
59
+ interface PartyRoleChangedPayload {
60
+ partyId: string;
61
+ role: PartyRole;
62
+ }
63
+ interface PartyUserLinkedPayload {
64
+ partyId: string;
65
+ userId: string;
66
+ }
67
+ interface PartyExternalRefAttachedPayload {
68
+ partyId: string;
69
+ sourceModel: string;
70
+ sourceId: string;
71
+ }
72
+ interface PartyLifecyclePayload {
73
+ partyId: string;
74
+ }
75
+ interface PartyMergedPayload {
76
+ /** The party that was merged away (now a redirect). */
77
+ sourceId: string;
78
+ /** The surviving party. */
79
+ targetId: string;
80
+ /** How many identity keys the survivor absorbed. */
81
+ absorbedKeys: number;
82
+ }
83
+ /** Name → payload map — subscribers get exact typing from the event name. */
84
+ interface PartyEventPayloads {
85
+ [PARTY_EVENTS.CREATED]: PartyCreatedPayload;
86
+ [PARTY_EVENTS.UPDATED]: PartyUpdatedPayload;
87
+ [PARTY_EVENTS.ROLE_ADDED]: PartyRoleChangedPayload;
88
+ [PARTY_EVENTS.ROLE_REMOVED]: PartyRoleChangedPayload;
89
+ [PARTY_EVENTS.USER_LINKED]: PartyUserLinkedPayload;
90
+ [PARTY_EVENTS.EXTERNAL_REF_ATTACHED]: PartyExternalRefAttachedPayload;
91
+ [PARTY_EVENTS.ARCHIVED]: PartyLifecyclePayload;
92
+ [PARTY_EVENTS.RESTORED]: PartyLifecyclePayload;
93
+ [PARTY_EVENTS.MERGED]: PartyMergedPayload;
94
+ }
95
+ //#endregion
96
+ //#region src/events/event-catalog.d.ts
97
+ interface PartyEventDefinition<TSchema extends z.ZodType = z.ZodType> {
98
+ name: string;
99
+ version: number;
100
+ description: string;
101
+ zodSchema: TSchema;
102
+ schema: Record<string, unknown>;
103
+ create(payload: z.infer<TSchema>, meta?: Partial<EventMeta>): DomainEvent<z.infer<TSchema>>;
104
+ }
105
+ declare const PartyCreated: PartyEventDefinition<z.ZodObject<{
106
+ partyId: z.ZodString;
107
+ kind: z.ZodEnum<{
108
+ company: "company";
109
+ group: "group";
110
+ person: "person";
111
+ }>;
112
+ displayName: z.ZodString;
113
+ roles: z.ZodArray<z.ZodEnum<{
114
+ contact: "contact";
115
+ customer: "customer";
116
+ employee: "employee";
117
+ partner: "partner";
118
+ supplier: "supplier";
119
+ }>>;
120
+ organizationId: z.ZodOptional<z.ZodString>;
121
+ }, z.core.$strip>>;
122
+ declare const PartyUpdated: PartyEventDefinition<z.ZodObject<{
123
+ partyId: z.ZodString;
124
+ changed: z.ZodArray<z.ZodString>;
125
+ }, z.core.$strip>>;
126
+ declare const PartyRoleAdded: PartyEventDefinition<z.ZodObject<{
127
+ partyId: z.ZodString;
128
+ role: z.ZodEnum<{
129
+ contact: "contact";
130
+ customer: "customer";
131
+ employee: "employee";
132
+ partner: "partner";
133
+ supplier: "supplier";
134
+ }>;
135
+ }, z.core.$strip>>;
136
+ declare const PartyRoleRemoved: PartyEventDefinition<z.ZodObject<{
137
+ partyId: z.ZodString;
138
+ role: z.ZodEnum<{
139
+ contact: "contact";
140
+ customer: "customer";
141
+ employee: "employee";
142
+ partner: "partner";
143
+ supplier: "supplier";
144
+ }>;
145
+ }, z.core.$strip>>;
146
+ declare const PartyUserLinked: PartyEventDefinition<z.ZodObject<{
147
+ partyId: z.ZodString;
148
+ userId: z.ZodString;
149
+ }, z.core.$strip>>;
150
+ declare const PartyExternalRefAttached: PartyEventDefinition<z.ZodObject<{
151
+ partyId: z.ZodString;
152
+ sourceModel: z.ZodString;
153
+ sourceId: z.ZodString;
154
+ }, z.core.$strip>>;
155
+ declare const PartyArchived: PartyEventDefinition<z.ZodObject<{
156
+ partyId: z.ZodString;
157
+ }, z.core.$strip>>;
158
+ declare const PartyRestored: PartyEventDefinition<z.ZodObject<{
159
+ partyId: z.ZodString;
160
+ }, z.core.$strip>>;
161
+ declare const PartyMerged: PartyEventDefinition<z.ZodObject<{
162
+ sourceId: z.ZodString;
163
+ targetId: z.ZodString;
164
+ absorbedKeys: z.ZodNumber;
165
+ }, z.core.$strip>>;
166
+ declare const partyEventDefinitions: ReadonlyArray<PartyEventDefinition>;
167
+ //#endregion
168
+ //#region src/domain/ports.d.ts
169
+ /**
170
+ * Operation context threaded through every domain verb. Extends primitives'
171
+ * `OperationContext`; narrows the id fields to what Mongoose stores cleanly.
172
+ */
173
+ interface PartyContext extends OperationContext {
174
+ organizationId?: string | Types.ObjectId;
175
+ actorId?: string | Types.ObjectId;
176
+ /** Mongo transaction session, passed straight to mongokit. */
177
+ session?: unknown;
178
+ /** Cooperative cancellation — threaded to mongokit at choke points. */
179
+ signal?: AbortSignal;
180
+ /** Retry policy for driver round-trips (mongokit `RetryPolicy`). */
181
+ retryPolicy?: unknown;
182
+ }
183
+ /** Minimal logger surface the in-process bus + repositories log through. */
184
+ interface PartyLogger {
185
+ info?(msg: string, meta?: Record<string, unknown>): void;
186
+ warn?(msg: string, meta?: Record<string, unknown>): void;
187
+ error?(msg: string, meta?: Record<string, unknown>): void;
188
+ debug?(msg: string, meta?: Record<string, unknown>): void;
189
+ }
190
+ //#endregion
191
+ //#region src/events/helpers.d.ts
192
+ declare function createEvent<T>(type: string, payload: T, ctx?: PartyContext, meta?: Partial<EventMeta>): DomainEvent<T>;
193
+ //#endregion
194
+ //#region src/events/in-process-bus.d.ts
195
+ declare class InProcessPartyBus implements EventTransport {
196
+ readonly name = "in-process-party";
197
+ private subs;
198
+ private readonly logger;
199
+ constructor(options?: {
200
+ logger?: PartyLogger | undefined;
201
+ });
202
+ publish(event: DomainEvent): Promise<void>;
203
+ subscribe(pattern: string, handler: EventHandler): Promise<() => void>;
204
+ close(): Promise<void>;
205
+ }
206
+ //#endregion
207
+ //#region src/events/dispatch.d.ts
208
+ /** Context key carrying a post-commit publish queue (attached by `withTransaction`). */
209
+ declare const PENDING_EVENTS: unique symbol;
210
+ interface PartyEventDispatcherOptions {
211
+ transport: EventTransport;
212
+ outbox?: OutboxStore | undefined;
213
+ logger?: PartyLogger | undefined;
214
+ }
215
+ declare class PartyEventDispatcher {
216
+ readonly name: string;
217
+ private readonly transport;
218
+ private readonly outbox;
219
+ private readonly logger;
220
+ constructor(opts: PartyEventDispatcherOptions);
221
+ /** Raw publish — hosts and the unit-of-work flush use this. */
222
+ publish(event: DomainEvent): Promise<void>;
223
+ subscribe(pattern: string, handler: EventHandler): Promise<() => void>;
224
+ /**
225
+ * Route a domain event with full §P8 discipline. Reads the pending queue and
226
+ * session off `ctx`.
227
+ */
228
+ dispatch(event: DomainEvent, ctx: PartyContext): Promise<void>;
229
+ close(): Promise<void>;
230
+ }
231
+ /**
232
+ * Drain a pending-events queue to the transport AFTER the owning transaction
233
+ * commits. Per-event failures are swallowed — durable delivery is the outbox's
234
+ * job (PACKAGE_RULES §P8.1).
235
+ */
236
+ declare function flushPending(dispatcher: PartyEventDispatcher, queue: DomainEvent[], logger?: PartyLogger): Promise<void>;
237
+ //#endregion
238
+ export { InProcessPartyBus, PARTY_EVENTS, PENDING_EVENTS, PartyArchived, PartyCreated, PartyCreatedPayload, PartyEventDefinition, PartyEventDispatcher, PartyEventDispatcherOptions, PartyEventName, PartyEventPayloads, PartyExternalRefAttached, PartyExternalRefAttachedPayload, PartyLifecyclePayload, PartyMerged, PartyMergedPayload, PartyRestored, PartyRoleAdded, PartyRoleChangedPayload, PartyRoleRemoved, PartyUpdated, PartyUpdatedPayload, PartyUserLinked, PartyUserLinkedPayload, createEvent, flushPending, partyEventDefinitions };
package/dist/events.js ADDED
@@ -0,0 +1,255 @@
1
+ import { z } from "zod";
2
+ import { createEvent as createEvent$1, matchEventPattern } from "@classytic/primitives/events";
3
+ //#region src/events/event-constants.ts
4
+ /** PACKAGE_RULES §12: `package:resource.verb`. Emit ONLY these catalog names. */
5
+ const PARTY_EVENTS = {
6
+ CREATED: "party:party.created",
7
+ UPDATED: "party:party.updated",
8
+ ROLE_ADDED: "party:party.role_added",
9
+ ROLE_REMOVED: "party:party.role_removed",
10
+ USER_LINKED: "party:party.user_linked",
11
+ EXTERNAL_REF_ATTACHED: "party:party.external_ref_attached",
12
+ ARCHIVED: "party:party.archived",
13
+ RESTORED: "party:party.restored",
14
+ MERGED: "party:party.merged"
15
+ };
16
+ //#endregion
17
+ //#region src/events/event-catalog.ts
18
+ /**
19
+ * Zod-source event catalog (PACKAGE_RULES §18). One Zod schema per event is the
20
+ * single source of truth — runtime validation, static types, and arc's
21
+ * `EventRegistry` (OpenAPI + `arc describe`) all derive from it. Structurally
22
+ * identical to arc's `EventDefinitionOutput`; we never import arc.
23
+ *
24
+ * Timestamps use `z.iso.datetime()`, never `z.coerce.date()` (§25).
25
+ */
26
+ function definePartyEvent(input) {
27
+ const { name, version = 1, description, zodSchema } = input;
28
+ return {
29
+ name,
30
+ version,
31
+ description,
32
+ zodSchema,
33
+ schema: z.toJSONSchema(zodSchema),
34
+ create(payload, meta) {
35
+ return createEvent$1(name, payload, {
36
+ source: "party",
37
+ ...meta
38
+ });
39
+ }
40
+ };
41
+ }
42
+ const partyKind = z.enum([
43
+ "person",
44
+ "company",
45
+ "group"
46
+ ]);
47
+ const partyRole = z.enum([
48
+ "customer",
49
+ "supplier",
50
+ "contact",
51
+ "employee",
52
+ "partner"
53
+ ]);
54
+ const createdSchema = z.object({
55
+ partyId: z.string(),
56
+ kind: partyKind,
57
+ displayName: z.string(),
58
+ roles: z.array(partyRole),
59
+ organizationId: z.string().optional()
60
+ });
61
+ const updatedSchema = z.object({
62
+ partyId: z.string(),
63
+ changed: z.array(z.string())
64
+ });
65
+ const roleChangedSchema = z.object({
66
+ partyId: z.string(),
67
+ role: partyRole
68
+ });
69
+ const userLinkedSchema = z.object({
70
+ partyId: z.string(),
71
+ userId: z.string()
72
+ });
73
+ const externalRefSchema = z.object({
74
+ partyId: z.string(),
75
+ sourceModel: z.string(),
76
+ sourceId: z.string()
77
+ });
78
+ const lifecycleSchema = z.object({ partyId: z.string() });
79
+ const mergedSchema = z.object({
80
+ sourceId: z.string(),
81
+ targetId: z.string(),
82
+ absorbedKeys: z.number().int().nonnegative()
83
+ });
84
+ const PartyCreated = definePartyEvent({
85
+ name: PARTY_EVENTS.CREATED,
86
+ description: "A new canonical party identity was created.",
87
+ zodSchema: createdSchema
88
+ });
89
+ const PartyUpdated = definePartyEvent({
90
+ name: PARTY_EVENTS.UPDATED,
91
+ description: "A party identity field changed.",
92
+ zodSchema: updatedSchema
93
+ });
94
+ const PartyRoleAdded = definePartyEvent({
95
+ name: PARTY_EVENTS.ROLE_ADDED,
96
+ description: "A role (customer/supplier/…) was granted to a party.",
97
+ zodSchema: roleChangedSchema
98
+ });
99
+ const PartyRoleRemoved = definePartyEvent({
100
+ name: PARTY_EVENTS.ROLE_REMOVED,
101
+ description: "A role was revoked from a party.",
102
+ zodSchema: roleChangedSchema
103
+ });
104
+ const PartyUserLinked = definePartyEvent({
105
+ name: PARTY_EVENTS.USER_LINKED,
106
+ description: "An auth user was linked to a party (1:many).",
107
+ zodSchema: userLinkedSchema
108
+ });
109
+ const PartyExternalRefAttached = definePartyEvent({
110
+ name: PARTY_EVENTS.EXTERNAL_REF_ATTACHED,
111
+ description: "An external-store reference was attached to a party (facet link).",
112
+ zodSchema: externalRefSchema
113
+ });
114
+ const PartyArchived = definePartyEvent({
115
+ name: PARTY_EVENTS.ARCHIVED,
116
+ description: "A party was archived.",
117
+ zodSchema: lifecycleSchema
118
+ });
119
+ const PartyRestored = definePartyEvent({
120
+ name: PARTY_EVENTS.RESTORED,
121
+ description: "An archived party was restored to active.",
122
+ zodSchema: lifecycleSchema
123
+ });
124
+ const PartyMerged = definePartyEvent({
125
+ name: PARTY_EVENTS.MERGED,
126
+ description: "A duplicate party was merged into a survivor; the source becomes a redirect.",
127
+ zodSchema: mergedSchema
128
+ });
129
+ const partyEventDefinitions = [
130
+ PartyCreated,
131
+ PartyUpdated,
132
+ PartyRoleAdded,
133
+ PartyRoleRemoved,
134
+ PartyUserLinked,
135
+ PartyExternalRefAttached,
136
+ PartyArchived,
137
+ PartyRestored,
138
+ PartyMerged
139
+ ];
140
+ //#endregion
141
+ //#region src/events/helpers.ts
142
+ /**
143
+ * `createEvent` — thin wrapper over primitives' `createEvent` that stamps
144
+ * `source: 'party'` and lifts `actorId`/`organizationId`/`correlationId` off the
145
+ * `PartyContext`. Use this instead of importing `createEvent` directly so the
146
+ * source stamp + meta stay consistent (PACKAGE_RULES §14).
147
+ */
148
+ function createEvent(type, payload, ctx, meta) {
149
+ return createEvent$1(type, payload, {
150
+ source: "party",
151
+ ...ctx?.actorId !== void 0 ? { actorId: String(ctx.actorId) } : {},
152
+ ...ctx?.organizationId !== void 0 ? { organizationId: String(ctx.organizationId) } : {},
153
+ ...ctx?.correlationId !== void 0 ? { correlationId: ctx.correlationId } : {},
154
+ ...meta
155
+ });
156
+ }
157
+ //#endregion
158
+ //#region src/events/in-process-bus.ts
159
+ /**
160
+ * In-process event bus — default fallback when no `eventTransport` is given to
161
+ * `createParty`. Structurally identical to arc's `MemoryEventTransport`;
162
+ * pattern-matching delegates to primitives' `matchEventPattern` (PACKAGE_RULES
163
+ * §13). Handler errors are swallowed-and-logged — emission never blocks writes.
164
+ */
165
+ var InProcessPartyBus = class {
166
+ name = "in-process-party";
167
+ subs = [];
168
+ logger;
169
+ constructor(options) {
170
+ this.logger = options?.logger;
171
+ }
172
+ async publish(event) {
173
+ const matching = this.subs.filter((s) => matchEventPattern(s.pattern, event.type));
174
+ for (const sub of matching) try {
175
+ await sub.handler(event);
176
+ } catch (err) {
177
+ this.logger?.error?.(`[party] event handler error [${event.type}]`, { error: err instanceof Error ? err.message : String(err) });
178
+ }
179
+ }
180
+ async subscribe(pattern, handler) {
181
+ const sub = {
182
+ pattern,
183
+ handler
184
+ };
185
+ this.subs.push(sub);
186
+ return () => {
187
+ this.subs = this.subs.filter((s) => s !== sub);
188
+ };
189
+ }
190
+ async close() {
191
+ this.subs = [];
192
+ }
193
+ };
194
+ //#endregion
195
+ //#region src/events/dispatch.ts
196
+ /** Context key carrying a post-commit publish queue (attached by `withTransaction`). */
197
+ const PENDING_EVENTS = Symbol.for("@classytic/party:pendingEvents");
198
+ var PartyEventDispatcher = class {
199
+ name;
200
+ transport;
201
+ outbox;
202
+ logger;
203
+ constructor(opts) {
204
+ this.transport = opts.transport;
205
+ this.outbox = opts.outbox;
206
+ this.logger = opts.logger;
207
+ this.name = opts.transport.name;
208
+ }
209
+ /** Raw publish — hosts and the unit-of-work flush use this. */
210
+ publish(event) {
211
+ return this.transport.publish(event);
212
+ }
213
+ subscribe(pattern, handler) {
214
+ return this.transport.subscribe(pattern, handler);
215
+ }
216
+ /**
217
+ * Route a domain event with full §P8 discipline. Reads the pending queue and
218
+ * session off `ctx`.
219
+ */
220
+ async dispatch(event, ctx) {
221
+ if (this.outbox) try {
222
+ await this.outbox.save(event, ctx.session !== void 0 ? { session: ctx.session } : {});
223
+ } catch (err) {
224
+ this.logger?.error?.(`[party] outbox.save failed for ${event.type}`, { error: err instanceof Error ? err.message : String(err) });
225
+ throw err;
226
+ }
227
+ const queue = ctx[PENDING_EVENTS];
228
+ if (Array.isArray(queue)) {
229
+ queue.push(event);
230
+ return;
231
+ }
232
+ try {
233
+ await this.transport.publish(event);
234
+ } catch (err) {
235
+ this.logger?.error?.(`[party] publish failed for ${event.type}`, { error: err instanceof Error ? err.message : String(err) });
236
+ }
237
+ }
238
+ async close() {
239
+ await this.transport.close?.();
240
+ }
241
+ };
242
+ /**
243
+ * Drain a pending-events queue to the transport AFTER the owning transaction
244
+ * commits. Per-event failures are swallowed — durable delivery is the outbox's
245
+ * job (PACKAGE_RULES §P8.1).
246
+ */
247
+ async function flushPending(dispatcher, queue, logger) {
248
+ for (const event of queue) try {
249
+ await dispatcher.publish(event);
250
+ } catch (err) {
251
+ logger?.error?.(`[party] post-commit publish failed for ${event.type}`, { error: err instanceof Error ? err.message : String(err) });
252
+ }
253
+ }
254
+ //#endregion
255
+ export { InProcessPartyBus, PARTY_EVENTS, PENDING_EVENTS, PartyArchived, PartyCreated, PartyEventDispatcher, PartyExternalRefAttached, PartyMerged, PartyRestored, PartyRoleAdded, PartyRoleRemoved, PartyUpdated, PartyUserLinked, createEvent, flushPending, partyEventDefinitions };