@i4e/invest4edu-access-core 0.42.1 → 0.43.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@i4e/invest4edu-access-core",
3
- "version": "0.42.1",
3
+ "version": "0.43.0",
4
4
  "description": "Shared access-control primitives for NeoFindesk: tenant keystone, role capabilities, reportee tree, entity/role vocabulary, mobile app identity, feature flags, and the unified access engine (registry schema, snapshot resolver, visibleWhen).",
5
5
  "type": "module",
6
6
  "exports": {
@@ -56,10 +56,10 @@ export function createClientProfileService({ getDb, getSettings, getSideContext
56
56
  * `mode` tells the caller whether to act on it: "enforce" → show the existing client instead
57
57
  * of creating; "audit" → carry on as before, the match was only reported.
58
58
  */
59
- async function findExisting({ pan, profileType, session }) {
59
+ async function findExisting({ pan, profileType, distributorId = null, session }) {
60
60
  const settings = getSettings();
61
61
  if (!isEvaluated(settings, "resolve_by_pan")) return { profile: null, mode: "off" };
62
- const profile = pickProfileOfType(await findActiveProfilesByPan(pan, { session }), profileType);
62
+ const profile = pickProfileOfType(await findActiveProfilesByPan(pan, { session }), profileType, { distributorId, settings, ctx: getSideContext() });
63
63
  const mode = isEnforced(settings, "resolve_by_pan") ? "enforce" : "audit";
64
64
  if (profile && mode === "audit") onAudit({ kind: "resolve_by_pan", client_id: profile._id, profile_type: profileType });
65
65
  return { profile, mode };
@@ -65,9 +65,25 @@ export function profileTypeForCaller(callerDistributor, settings) {
65
65
  return profileTypeFor(callerDistributor, settings);
66
66
  }
67
67
 
68
+ /** Corporate profiles are one per corporate firm and never shared (settings.corporate). */
69
+ function corporateSiloed(settings) {
70
+ return settings?.corporate?.one_profile_per_distributor !== false;
71
+ }
72
+
73
+ /** The side that owns a profile: its primary mapping, else its distributor_id. */
74
+ function primarySideOfProfile(profile, ctx) {
75
+ return sideKeyOf((profile?.distributor_mappings ?? []).find((m) => m?.is_primary)?.distributor_id ?? profile?.distributor_id, ctx);
76
+ }
77
+
68
78
  /** Of a person's active profiles, the one of this type (oldest first if the data has duplicates). */
69
- export function pickProfileOfType(profiles = [], profileType) {
70
- const ofType = profiles.filter((p) => (p.profile_type ?? PROFILE_TYPES.MAIN) === profileType);
79
+ export function pickProfileOfType(profiles = [], profileType, { distributorId = null, settings, ctx } = {}) {
80
+ let ofType = profiles.filter((p) => (p.profile_type ?? PROFILE_TYPES.MAIN) === profileType);
81
+ // A corporate profile belongs to ONE corporate firm: only that firm's own profile is "the existing
82
+ // one". Another firm (or a caller with no firm) finds nothing and creates its own.
83
+ if (profileType === PROFILE_TYPES.CORPORATE && corporateSiloed(settings)) {
84
+ const side = distributorId ? sideKeyOf(distributorId, ctx) : null;
85
+ ofType = side ? ofType.filter((p) => primarySideOfProfile(p, ctx) === side) : [];
86
+ }
71
87
  ofType.sort((a, b) => new Date(a.created_date ?? 0) - new Date(b.created_date ?? 0));
72
88
  return ofType[0] ?? null;
73
89
  }
@@ -90,9 +106,13 @@ export function attachDecision({ profile, actor = {}, via, at = new Date(), sett
90
106
  if (actor.callerClass === CALLER_CLASSES.PARTNER) {
91
107
  const side = sideKeyOf(actor.distributorId, ctx);
92
108
  if (side === DIRECT_SIDE) return { kind: "none", reason: "partner_without_firm" };
93
- const primarySide = sideKeyOf((profile.distributor_mappings ?? []).find((m) => m?.is_primary)?.distributor_id ?? profile.distributor_id, ctx);
109
+ const primarySide = primarySideOfProfile(profile, ctx);
94
110
  const mapped = side === primarySide || (profile.distributor_mappings ?? []).some((m) => m && m.status !== "inactive" && sideKeyOf(m.distributor_id, ctx) === side);
95
111
  if (mapped) return { kind: "none", reason: "already_mapped" };
112
+ // A corporate profile is its firm's alone: no other firm is ever added to it.
113
+ if ((profile.profile_type ?? PROFILE_TYPES.MAIN) === PROFILE_TYPES.CORPORATE && corporateSiloed(settings)) {
114
+ return { kind: "none", reason: "corporate_silo" };
115
+ }
96
116
  return {
97
117
  kind: "mapping",
98
118
  reason: "new_side",
@@ -77,6 +77,12 @@ export const DEFAULT_CLIENT_PROFILE_SETTINGS = deepFreeze({
77
77
  },
78
78
  corporate_distributor_types: [],
79
79
  house_distributor_codes: [],
80
+ corporate: {
81
+ // Each corporate distributor holds its OWN profile of a person (one per corporate firm), and
82
+ // works on it alone: a PAN lookup never surfaces another firm's corporate profile, and no
83
+ // other firm is ever attached to one.
84
+ one_profile_per_distributor: true,
85
+ },
80
86
  primary: {
81
87
  lock_on_first_order: true,
82
88
  ignore_order_statuses: [],
@@ -192,6 +198,7 @@ export const CLIENT_PROFILE_SETTING_DESCRIPTIONS = Object.freeze({
192
198
  "modes.detail_scope": "A client's full record by id only for callers whose client list includes them; everyone else gets the limited card (R15).",
193
199
  "modes.linked_visibility": "Clients reached only through a secondary mapping or a linked user appear in lists, as the limited card.",
194
200
  corporate_distributor_types: "distributors.distributor_type values whose clients get a corporate profile.",
201
+ "corporate.one_profile_per_distributor": "Each corporate firm has its own, siloed profile of a person: no cross-firm lookup or attach.",
195
202
  house_distributor_codes: "Distributor codes treated as direct (InvestValue's own house distributors).",
196
203
  "primary.lock_on_first_order": "The first non-ignored order locks the primary distributor.",
197
204
  "primary.ignore_order_statuses": "Order statuses that do not count as first business (e.g. cancelled).",