@classytic/party 0.1.0 → 0.3.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 +83 -0
- package/LICENSE +1 -1
- package/README.md +29 -4
- package/dist/events.d.ts +46 -9
- package/dist/events.js +60 -34
- package/dist/index.d.ts +410 -90
- package/dist/index.js +1000 -382
- package/dist/maintenance.d.ts +216 -0
- package/dist/maintenance.js +353 -0
- package/package.json +21 -10
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,89 @@
|
|
|
3
3
|
All notable changes to `@classytic/party` are documented here. Adheres to
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/) and [SemVer](https://semver.org/).
|
|
5
5
|
|
|
6
|
+
## [0.3.0] — 2026-10-11
|
|
7
|
+
|
|
8
|
+
### Added — marketing consent, per contact channel
|
|
9
|
+
|
|
10
|
+
- `ContactPoint.consent` (`{ status: 'granted' | 'withdrawn', at, source?, ip? }`) records permission to MARKET to one channel. Absent means "never asked", which a sender must treat as a refusal; `withdrawn` is deliberately not "absent", because only an opt-out has to survive a merge, a re-import or the person booking again.
|
|
11
|
+
- `recordConsent(id, input, ctx)` stores a decision against a channel, matching the channel by its NORMALIZED value (`07700 900000` and `+447700900000` reach the same contact point). A channel the party does not hold is a typed error, not a silent no-op. Emits `party:party.consent_recorded`.
|
|
12
|
+
- Merging two parties **reconciles consent** (`reconcileConsent`, exported): a withdrawal always beats a grant, whichever record is newer, because duplicate rows' clocks record when a row was touched, not when the person spoke. Between two records of the same status the later one wins. A channel one side verified stays verified. Before this, the loser's contact row was dropped wholesale and could discard an opt-out the duplicate alone carried.
|
|
13
|
+
- Contact points are copied field by field on merge: Mongoose subdocuments keep their values behind prototype getters, so spreading one yielded internals and dropped data.
|
|
14
|
+
- `CONSENT_STATUSES` / `ConsentStatus` and the `ChannelConsent` type.
|
|
15
|
+
|
|
16
|
+
### Added — dated related-party relationships
|
|
17
|
+
|
|
18
|
+
- `relationships[]` on a party: `shareholder`, `director`, `key_management`, `partner_in_firm`, `member`, `employee`, `lessor`, `associate` (`RELATIONSHIP_KINDS`), each with `from` and an exclusive `to`, an optional `relatedTo` (absent means the reporting company), and `shareClass` / `sharePercent` for shareholders. One party can hold several at once.
|
|
19
|
+
- `addRelationship`, `endRelationship` and `relationshipsOn(id, date, ctx)` (the relationships holding on a date). A bad date range, an overlap or an unknown row throws `PartyRelationshipError` (`party.relationship_invalid`, 422). Emits `party:party.relationship_changed`.
|
|
20
|
+
- A party model index for "every shareholder" / "every lessor" queries.
|
|
21
|
+
|
|
22
|
+
### Added — `legalForm`
|
|
23
|
+
|
|
24
|
+
`legalForm` on a party (`@classytic/primitives/legal-form`): what the law treats the party as — a sole trader is an `individual`. Absent means not stated.
|
|
25
|
+
|
|
26
|
+
### Changed — peer floors raised to current
|
|
27
|
+
|
|
28
|
+
`@classytic/mongokit` `>=3.33.0` → **`>=3.41.0`**, `@classytic/primitives` `>=0.29.0` → **`>=0.31.1`**, `@classytic/repo-core` `>=0.24.0` → **`>=0.31.0`**, `@classytic/contact` `>=0.1.0` → **`>=0.2.0`**, `mongoose` `>=9.4.1` → **`>=9.11.0`** (mongokit 3.41 requires it). Verified on a fresh install. New parties and merges write the new fields; existing documents read unchanged (`consent` stays absent, `relationships` defaults to `[]`).
|
|
29
|
+
|
|
30
|
+
## [0.2.0] — 2026-08-17
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
- `defineParty(shape)` / `blueprint.bind(connection, runtime)` — describe/bind construction API replacing `createParty`; describe phase is pure and safe at import time
|
|
34
|
+
- `SubjectRef` / `PARTY_MODEL_NAME` exported from `./domain/subject` — canonical polymorphic subject seam for cross-kernel refs (`@classytic/access`, `@classytic/muster`, etc.)
|
|
35
|
+
- `src/maintenance/phone-identity-repair.ts` — repair utility for phone-identity drift
|
|
36
|
+
|
|
37
|
+
### Removed
|
|
38
|
+
- `createParty` (replaced by `defineParty`)
|
|
39
|
+
|
|
40
|
+
## 1.0.0 - 2026-08-07 (unpublished)
|
|
41
|
+
|
|
42
|
+
### Requirements
|
|
43
|
+
|
|
44
|
+
- **New peer dependency: `@classytic/contact >=0.1.0`** — install it alongside
|
|
45
|
+
this package. It owns contact-point canonicalisation (E.164 phones, emails),
|
|
46
|
+
which `resolveOrCreate` / `syncIdentity` derive their `identityKeys` from.
|
|
47
|
+
A PEER rather than a regular dependency on purpose: npm may nest a regular one
|
|
48
|
+
under `party/node_modules`, giving the host a second module instance, and then
|
|
49
|
+
`err instanceof PhoneFormatError` is `false` at a host catch site that imported
|
|
50
|
+
its own copy — a guard that compiles, runs, and matches nothing.
|
|
51
|
+
|
|
52
|
+
### Breaking
|
|
53
|
+
|
|
54
|
+
- **Contact input that cannot be canonicalised now THROWS** instead of becoming
|
|
55
|
+
an identity key. A phone that is not a valid, dialable number raises
|
|
56
|
+
`PartyPhoneFormatError` (400 / `party.phone_invalid`); an unparseable email
|
|
57
|
+
raises `EmailFormatError`. `0.1.0` degraded both — a rejected phone became
|
|
58
|
+
`+${digits}`, producing keys like `phone:+01309000993` that are neither a
|
|
59
|
+
national number nor E.164, so two spellings of one person keyed differently and
|
|
60
|
+
the identity forked silently. There is no lenient variant.
|
|
61
|
+
- **Phone canonicalisation requires a region for national-format input.**
|
|
62
|
+
`PartyShape.defaultPhoneRegion` (ISO 3166-1 alpha-2, supplied by the
|
|
63
|
+
deployment — this kernel names no country). Unset ⇒ E.164-only input is
|
|
64
|
+
accepted and a national format is refused rather than guessed at. An explicit
|
|
65
|
+
`+…` number is never re-read through the region.
|
|
66
|
+
- **`ContactPoint.value` stores the canonical E.164**, with the caller's original
|
|
67
|
+
spelling preserved as `ContactPoint.input` (never keyed, never matched on).
|
|
68
|
+
|
|
69
|
+
### Changed
|
|
70
|
+
|
|
71
|
+
- Added `PartyShape.identityContactTypes` (`email | phone`, both by default).
|
|
72
|
+
A host can exclude shared contact channels from automatic identity resolution
|
|
73
|
+
while retaining normalized contact points. Tax identifiers and external
|
|
74
|
+
references remain authoritative. Existing defaults are unchanged.
|
|
75
|
+
- The unique `identityKeys` index now explicitly includes only non-empty arrays.
|
|
76
|
+
This permits multiple name-only or contact-only parties when contact channels
|
|
77
|
+
are non-authoritative, without weakening uniqueness for real identity keys.
|
|
78
|
+
- Canonical email output is unchanged for every valid address — trim plus
|
|
79
|
+
lowercase, and still **no** Gmail-dot or `+tag` folding, since folding a
|
|
80
|
+
provider's rule merges two distinct inboxes into one identity. No existing key
|
|
81
|
+
is re-derived.
|
|
82
|
+
- `libphonenumber-js` is no longer a direct dependency.
|
|
83
|
+
|
|
84
|
+
## 0.1.1 - 2026-07-22 (unpublished)
|
|
85
|
+
|
|
86
|
+
### Changed
|
|
87
|
+
- Internal: `InProcess*Bus` is now a thin subclass of `@classytic/primitives/event-infra`'s shared `InProcessEventBus` (dedup / error-isolation / close semantics unchanged); `MemoryOutboxStore` re-exported from the shared runtime where applicable. No public API, event-name, or wire-envelope change. Requires `@classytic/primitives >=0.13.0`.
|
|
88
|
+
|
|
6
89
|
## [0.1.0] - 2026-07-13
|
|
7
90
|
|
|
8
91
|
Initial release — the Party / Business-Partner identity kernel.
|
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -48,12 +48,12 @@ Peers: `@classytic/mongokit >=3.20`, `@classytic/primitives >=0.9.1`,
|
|
|
48
48
|
|
|
49
49
|
```ts
|
|
50
50
|
import mongoose from 'mongoose';
|
|
51
|
-
import {
|
|
51
|
+
import { defineParty } from '@classytic/party';
|
|
52
52
|
|
|
53
|
-
const party =
|
|
54
|
-
connection: mongoose.connection,
|
|
53
|
+
const party = defineParty({
|
|
55
54
|
multiTenant: false, // identity is company-wide here; pass true for per-tenant SaaS
|
|
56
|
-
|
|
55
|
+
defaultPhoneRegion: 'BD',
|
|
56
|
+
}).bind(mongoose.connection);
|
|
57
57
|
|
|
58
58
|
// Resolve-or-create by ANY identity signal — race-safe, dedup by email/phone/vat.
|
|
59
59
|
const { party: ada, created } = await party.port.resolveOrCreate(
|
|
@@ -76,6 +76,27 @@ const snap = await party.port.snapshot(String(ada._id), {});
|
|
|
76
76
|
await party.repositories.party.merge(dupId, survivorId, {});
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
+
### Contacts are not always proof of identity
|
|
80
|
+
|
|
81
|
+
Email and phone are authoritative identity keys by default, preserving the
|
|
82
|
+
race-safe behavior existing commerce hosts rely on. Domains where people often
|
|
83
|
+
share contact details can keep those values as contact data without allowing
|
|
84
|
+
them to collapse two parties:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
const party = defineParty({
|
|
88
|
+
multiTenant: true,
|
|
89
|
+
defaultPhoneRegion: 'GB',
|
|
90
|
+
identityContactTypes: [],
|
|
91
|
+
}).bind(mongoose.connection);
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Tax identifiers and explicit external references remain authoritative. A
|
|
95
|
+
domain-specific facet (for example a legal Client) can perform candidate-match
|
|
96
|
+
review and then attach its stable external reference or explicitly merge the
|
|
97
|
+
confirmed duplicate. This setting shapes persisted `identityKeys`; migrate
|
|
98
|
+
existing rows before changing it on a live deployment.
|
|
99
|
+
|
|
79
100
|
## The `PartyPort` — the reusable contract
|
|
80
101
|
|
|
81
102
|
Consuming domains depend on `engine.port` (a `PartyPort`), not the models:
|
|
@@ -111,3 +132,7 @@ entities — that's the duplication this kernel exists to remove.
|
|
|
111
132
|
## License
|
|
112
133
|
|
|
113
134
|
MIT
|
|
135
|
+
|
|
136
|
+
## Trademark
|
|
137
|
+
|
|
138
|
+
Code is MIT-licensed. "Classytic"/"arc" names + logos are trademarks of Classytic LLC — see [TRADEMARK.md](TRADEMARK.md).
|
package/dist/events.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { DomainEvent, EventHandler, EventMeta, EventTransport } from "@classytic/primitives/events";
|
|
3
|
+
import { InProcessEventBus } from "@classytic/primitives/event-infra";
|
|
3
4
|
import { Types } from "mongoose";
|
|
4
5
|
import { OperationContext } from "@classytic/primitives/context";
|
|
5
6
|
import "@classytic/primitives/reference";
|
|
@@ -14,10 +15,12 @@ declare const PARTY_EVENTS: {
|
|
|
14
15
|
readonly ROLE_ADDED: 'party:party.role_added';
|
|
15
16
|
readonly ROLE_REMOVED: 'party:party.role_removed';
|
|
16
17
|
readonly USER_LINKED: 'party:party.user_linked';
|
|
18
|
+
readonly CONSENT_RECORDED: 'party:party.consent_recorded';
|
|
17
19
|
readonly EXTERNAL_REF_ATTACHED: 'party:party.external_ref_attached';
|
|
18
20
|
readonly ARCHIVED: 'party:party.archived';
|
|
19
21
|
readonly RESTORED: 'party:party.restored';
|
|
20
22
|
readonly MERGED: 'party:party.merged';
|
|
23
|
+
readonly RELATIONSHIP_CHANGED: 'party:party.relationship_changed';
|
|
21
24
|
};
|
|
22
25
|
type PartyEventName = (typeof PARTY_EVENTS)[keyof typeof PARTY_EVENTS];
|
|
23
26
|
//#endregion
|
|
@@ -64,6 +67,14 @@ interface PartyUserLinkedPayload {
|
|
|
64
67
|
partyId: string;
|
|
65
68
|
userId: string;
|
|
66
69
|
}
|
|
70
|
+
interface PartyConsentRecordedPayload {
|
|
71
|
+
partyId: string;
|
|
72
|
+
channel: 'email' | 'phone';
|
|
73
|
+
/** The NORMALIZED channel — what a sender would actually dial or mail. */
|
|
74
|
+
value: string;
|
|
75
|
+
status: 'granted' | 'withdrawn';
|
|
76
|
+
source?: string;
|
|
77
|
+
}
|
|
67
78
|
interface PartyExternalRefAttachedPayload {
|
|
68
79
|
partyId: string;
|
|
69
80
|
sourceModel: string;
|
|
@@ -87,6 +98,7 @@ interface PartyEventPayloads {
|
|
|
87
98
|
[PARTY_EVENTS.ROLE_ADDED]: PartyRoleChangedPayload;
|
|
88
99
|
[PARTY_EVENTS.ROLE_REMOVED]: PartyRoleChangedPayload;
|
|
89
100
|
[PARTY_EVENTS.USER_LINKED]: PartyUserLinkedPayload;
|
|
101
|
+
[PARTY_EVENTS.CONSENT_RECORDED]: PartyConsentRecordedPayload;
|
|
90
102
|
[PARTY_EVENTS.EXTERNAL_REF_ATTACHED]: PartyExternalRefAttachedPayload;
|
|
91
103
|
[PARTY_EVENTS.ARCHIVED]: PartyLifecyclePayload;
|
|
92
104
|
[PARTY_EVENTS.RESTORED]: PartyLifecyclePayload;
|
|
@@ -147,6 +159,19 @@ declare const PartyUserLinked: PartyEventDefinition<z.ZodObject<{
|
|
|
147
159
|
partyId: z.ZodString;
|
|
148
160
|
userId: z.ZodString;
|
|
149
161
|
}, z.core.$strip>>;
|
|
162
|
+
declare const PartyConsentRecorded: PartyEventDefinition<z.ZodObject<{
|
|
163
|
+
partyId: z.ZodString;
|
|
164
|
+
channel: z.ZodEnum<{
|
|
165
|
+
email: "email";
|
|
166
|
+
phone: "phone";
|
|
167
|
+
}>;
|
|
168
|
+
value: z.ZodString;
|
|
169
|
+
status: z.ZodEnum<{
|
|
170
|
+
granted: "granted";
|
|
171
|
+
withdrawn: "withdrawn";
|
|
172
|
+
}>;
|
|
173
|
+
source: z.ZodOptional<z.ZodString>;
|
|
174
|
+
}, z.core.$strip>>;
|
|
150
175
|
declare const PartyExternalRefAttached: PartyEventDefinition<z.ZodObject<{
|
|
151
176
|
partyId: z.ZodString;
|
|
152
177
|
sourceModel: z.ZodString;
|
|
@@ -163,6 +188,24 @@ declare const PartyMerged: PartyEventDefinition<z.ZodObject<{
|
|
|
163
188
|
targetId: z.ZodString;
|
|
164
189
|
absorbedKeys: z.ZodNumber;
|
|
165
190
|
}, z.core.$strip>>;
|
|
191
|
+
declare const PartyRelationshipChanged: PartyEventDefinition<z.ZodObject<{
|
|
192
|
+
partyId: z.ZodString;
|
|
193
|
+
relationshipId: z.ZodString;
|
|
194
|
+
kind: z.ZodEnum<{
|
|
195
|
+
associate: "associate";
|
|
196
|
+
director: "director";
|
|
197
|
+
employee: "employee";
|
|
198
|
+
key_management: "key_management";
|
|
199
|
+
lessor: "lessor";
|
|
200
|
+
member: "member";
|
|
201
|
+
partner_in_firm: "partner_in_firm";
|
|
202
|
+
shareholder: "shareholder";
|
|
203
|
+
}>;
|
|
204
|
+
change: z.ZodEnum<{
|
|
205
|
+
added: "added";
|
|
206
|
+
ended: "ended";
|
|
207
|
+
}>;
|
|
208
|
+
}, z.core.$strip>>;
|
|
166
209
|
declare const partyEventDefinitions: ReadonlyArray<PartyEventDefinition>;
|
|
167
210
|
//#endregion
|
|
168
211
|
//#region src/domain/ports.d.ts
|
|
@@ -192,16 +235,10 @@ interface PartyLogger {
|
|
|
192
235
|
declare function createEvent<T>(type: string, payload: T, ctx?: PartyContext, meta?: Partial<EventMeta>): DomainEvent<T>;
|
|
193
236
|
//#endregion
|
|
194
237
|
//#region src/events/in-process-bus.d.ts
|
|
195
|
-
declare class InProcessPartyBus
|
|
196
|
-
readonly name = "in-process-party";
|
|
197
|
-
private subs;
|
|
198
|
-
private readonly logger;
|
|
238
|
+
declare class InProcessPartyBus extends InProcessEventBus {
|
|
199
239
|
constructor(options?: {
|
|
200
|
-
logger?: PartyLogger
|
|
240
|
+
logger?: PartyLogger;
|
|
201
241
|
});
|
|
202
|
-
publish(event: DomainEvent): Promise<void>;
|
|
203
|
-
subscribe(pattern: string, handler: EventHandler): Promise<() => void>;
|
|
204
|
-
close(): Promise<void>;
|
|
205
242
|
}
|
|
206
243
|
//#endregion
|
|
207
244
|
//#region src/events/dispatch.d.ts
|
|
@@ -235,4 +272,4 @@ declare class PartyEventDispatcher {
|
|
|
235
272
|
*/
|
|
236
273
|
declare function flushPending(dispatcher: PartyEventDispatcher, queue: DomainEvent[], logger?: PartyLogger): Promise<void>;
|
|
237
274
|
//#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 };
|
|
275
|
+
export { InProcessPartyBus, PARTY_EVENTS, PENDING_EVENTS, PartyArchived, PartyConsentRecorded, PartyConsentRecordedPayload, PartyCreated, PartyCreatedPayload, PartyEventDefinition, PartyEventDispatcher, PartyEventDispatcherOptions, PartyEventName, PartyEventPayloads, PartyExternalRefAttached, PartyExternalRefAttachedPayload, PartyLifecyclePayload, PartyMerged, PartyMergedPayload, PartyRelationshipChanged, PartyRestored, PartyRoleAdded, PartyRoleChangedPayload, PartyRoleRemoved, PartyUpdated, PartyUpdatedPayload, PartyUserLinked, PartyUserLinkedPayload, createEvent, flushPending, partyEventDefinitions };
|
package/dist/events.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { createEvent as createEvent$1
|
|
2
|
+
import { createEvent as createEvent$1 } from "@classytic/primitives/events";
|
|
3
|
+
import { InProcessEventBus } from "@classytic/primitives/event-infra";
|
|
3
4
|
//#region src/events/event-constants.ts
|
|
4
5
|
/** PACKAGE_RULES §12: `package:resource.verb`. Emit ONLY these catalog names. */
|
|
5
6
|
const PARTY_EVENTS = {
|
|
@@ -8,10 +9,12 @@ const PARTY_EVENTS = {
|
|
|
8
9
|
ROLE_ADDED: "party:party.role_added",
|
|
9
10
|
ROLE_REMOVED: "party:party.role_removed",
|
|
10
11
|
USER_LINKED: "party:party.user_linked",
|
|
12
|
+
CONSENT_RECORDED: "party:party.consent_recorded",
|
|
11
13
|
EXTERNAL_REF_ATTACHED: "party:party.external_ref_attached",
|
|
12
14
|
ARCHIVED: "party:party.archived",
|
|
13
15
|
RESTORED: "party:party.restored",
|
|
14
|
-
MERGED: "party:party.merged"
|
|
16
|
+
MERGED: "party:party.merged",
|
|
17
|
+
RELATIONSHIP_CHANGED: "party:party.relationship_changed"
|
|
15
18
|
};
|
|
16
19
|
//#endregion
|
|
17
20
|
//#region src/events/event-catalog.ts
|
|
@@ -70,6 +73,13 @@ const userLinkedSchema = z.object({
|
|
|
70
73
|
partyId: z.string(),
|
|
71
74
|
userId: z.string()
|
|
72
75
|
});
|
|
76
|
+
const consentRecordedSchema = z.object({
|
|
77
|
+
partyId: z.string(),
|
|
78
|
+
channel: z.enum(["email", "phone"]),
|
|
79
|
+
value: z.string(),
|
|
80
|
+
status: z.enum(["granted", "withdrawn"]),
|
|
81
|
+
source: z.string().optional()
|
|
82
|
+
});
|
|
73
83
|
const externalRefSchema = z.object({
|
|
74
84
|
partyId: z.string(),
|
|
75
85
|
sourceModel: z.string(),
|
|
@@ -106,6 +116,11 @@ const PartyUserLinked = definePartyEvent({
|
|
|
106
116
|
description: "An auth user was linked to a party (1:many).",
|
|
107
117
|
zodSchema: userLinkedSchema
|
|
108
118
|
});
|
|
119
|
+
const PartyConsentRecorded = definePartyEvent({
|
|
120
|
+
name: PARTY_EVENTS.CONSENT_RECORDED,
|
|
121
|
+
description: "A marketing-consent decision was recorded against one contact channel.",
|
|
122
|
+
zodSchema: consentRecordedSchema
|
|
123
|
+
});
|
|
109
124
|
const PartyExternalRefAttached = definePartyEvent({
|
|
110
125
|
name: PARTY_EVENTS.EXTERNAL_REF_ATTACHED,
|
|
111
126
|
description: "An external-store reference was attached to a party (facet link).",
|
|
@@ -126,16 +141,38 @@ const PartyMerged = definePartyEvent({
|
|
|
126
141
|
description: "A duplicate party was merged into a survivor; the source becomes a redirect.",
|
|
127
142
|
zodSchema: mergedSchema
|
|
128
143
|
});
|
|
144
|
+
const relationshipChangedSchema = z.object({
|
|
145
|
+
partyId: z.string(),
|
|
146
|
+
relationshipId: z.string(),
|
|
147
|
+
kind: z.enum([
|
|
148
|
+
"shareholder",
|
|
149
|
+
"director",
|
|
150
|
+
"key_management",
|
|
151
|
+
"partner_in_firm",
|
|
152
|
+
"member",
|
|
153
|
+
"employee",
|
|
154
|
+
"lessor",
|
|
155
|
+
"associate"
|
|
156
|
+
]),
|
|
157
|
+
change: z.enum(["added", "ended"])
|
|
158
|
+
});
|
|
159
|
+
const PartyRelationshipChanged = definePartyEvent({
|
|
160
|
+
name: PARTY_EVENTS.RELATIONSHIP_CHANGED,
|
|
161
|
+
description: "A dated relationship of the party (shareholder, director, lessor, …) was added or ended.",
|
|
162
|
+
zodSchema: relationshipChangedSchema
|
|
163
|
+
});
|
|
129
164
|
const partyEventDefinitions = [
|
|
130
165
|
PartyCreated,
|
|
131
166
|
PartyUpdated,
|
|
132
167
|
PartyRoleAdded,
|
|
133
168
|
PartyRoleRemoved,
|
|
134
169
|
PartyUserLinked,
|
|
170
|
+
PartyConsentRecorded,
|
|
135
171
|
PartyExternalRefAttached,
|
|
136
172
|
PartyArchived,
|
|
137
173
|
PartyRestored,
|
|
138
|
-
PartyMerged
|
|
174
|
+
PartyMerged,
|
|
175
|
+
PartyRelationshipChanged
|
|
139
176
|
];
|
|
140
177
|
//#endregion
|
|
141
178
|
//#region src/events/helpers.ts
|
|
@@ -158,37 +195,24 @@ function createEvent(type, payload, ctx, meta) {
|
|
|
158
195
|
//#region src/events/in-process-bus.ts
|
|
159
196
|
/**
|
|
160
197
|
* In-process event bus — default fallback when no `eventTransport` is given to
|
|
161
|
-
* `createParty`.
|
|
162
|
-
*
|
|
163
|
-
*
|
|
198
|
+
* `createParty`.
|
|
199
|
+
*
|
|
200
|
+
* A thin subclass of `@classytic/primitives/event-infra`'s shared
|
|
201
|
+
* `InProcessEventBus`. Prior semantics preserved: glob matching via
|
|
202
|
+
* `matchEventPattern`, per-handler error isolation, idempotent `close()`.
|
|
203
|
+
* Party historically SWALLOWED handler errors when no logger was supplied —
|
|
204
|
+
* that default is preserved by passing `logger: null` unless the host injects
|
|
205
|
+
* a `PartyLogger`, whose structured `error(msg, meta)` slot is bridged onto
|
|
206
|
+
* the shared error-logger contract.
|
|
164
207
|
*/
|
|
165
|
-
var InProcessPartyBus = class {
|
|
166
|
-
name = "in-process-party";
|
|
167
|
-
subs = [];
|
|
168
|
-
logger;
|
|
208
|
+
var InProcessPartyBus = class extends InProcessEventBus {
|
|
169
209
|
constructor(options) {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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 = [];
|
|
210
|
+
const partyLogger = options?.logger;
|
|
211
|
+
super({
|
|
212
|
+
name: "in-process-party",
|
|
213
|
+
logLabel: "party",
|
|
214
|
+
logger: partyLogger?.error ? { error: (message, ...args) => partyLogger.error?.(message, { args }) } : null
|
|
215
|
+
});
|
|
192
216
|
}
|
|
193
217
|
};
|
|
194
218
|
//#endregion
|
|
@@ -211,7 +235,9 @@ var PartyEventDispatcher = class {
|
|
|
211
235
|
return this.transport.publish(event);
|
|
212
236
|
}
|
|
213
237
|
subscribe(pattern, handler) {
|
|
214
|
-
|
|
238
|
+
const subscribe = this.transport.subscribe?.bind(this.transport);
|
|
239
|
+
if (!subscribe) throw new Error("party: configured event transport does not support subscribe");
|
|
240
|
+
return subscribe(pattern, handler);
|
|
215
241
|
}
|
|
216
242
|
/**
|
|
217
243
|
* Route a domain event with full §P8 discipline. Reads the pending queue and
|
|
@@ -252,4 +278,4 @@ async function flushPending(dispatcher, queue, logger) {
|
|
|
252
278
|
}
|
|
253
279
|
}
|
|
254
280
|
//#endregion
|
|
255
|
-
export { InProcessPartyBus, PARTY_EVENTS, PENDING_EVENTS, PartyArchived, PartyCreated, PartyEventDispatcher, PartyExternalRefAttached, PartyMerged, PartyRestored, PartyRoleAdded, PartyRoleRemoved, PartyUpdated, PartyUserLinked, createEvent, flushPending, partyEventDefinitions };
|
|
281
|
+
export { InProcessPartyBus, PARTY_EVENTS, PENDING_EVENTS, PartyArchived, PartyConsentRecorded, PartyCreated, PartyEventDispatcher, PartyExternalRefAttached, PartyMerged, PartyRelationshipChanged, PartyRestored, PartyRoleAdded, PartyRoleRemoved, PartyUpdated, PartyUserLinked, createEvent, flushPending, partyEventDefinitions };
|