@i4e/invest4edu-access-core 0.38.0 → 0.40.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.38.0",
3
+ "version": "0.40.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": {
@@ -79,3 +79,4 @@ export {
79
79
  effectivePolicyFor,
80
80
  changedFields,
81
81
  } from "./field-policy.js";
82
+ export { UCC_RULE_NAME_OF_CLIENT_FIELD, uccFieldsTouched, uccSyncedRuleNames } from "./ucc-followup.js";
@@ -71,6 +71,7 @@ export const DEFAULT_CLIENT_PROFILE_SETTINGS = deepFreeze({
71
71
  resolve_by_pan: MODES.OFF,
72
72
  linked_visibility: MODES.OFF,
73
73
  mf_one_investor: MODES.OFF,
74
+ detail_scope: MODES.OFF,
74
75
  },
75
76
  corporate_distributor_types: [],
76
77
  house_distributor_codes: [],
@@ -123,6 +124,15 @@ export const DEFAULT_CLIENT_PROFILE_SETTINGS = deepFreeze({
123
124
  change_request: {
124
125
  ttl_minutes: 10,
125
126
  },
127
+ corrections: {
128
+ // Details a client cannot change in the app; asking sends their RM a WhatsApp and an email.
129
+ fields: ["client_name", "pan_card_no", "primary_mobile"],
130
+ },
131
+ ucc_followup: {
132
+ // A client's app edit to one of this product's UCC fields is never pushed to BSE by the client:
133
+ // the profile is flagged for backoffice / the RM to run the UCC update (uccfieldrules).
134
+ product_code: "mf",
135
+ },
126
136
  goals: {
127
137
  partner_goal_requires_approval: true,
128
138
  corporate_goals_stay_with_corporate: true,
@@ -136,6 +146,9 @@ export const DEFAULT_CLIENT_PROFILE_SETTINGS = deepFreeze({
136
146
  goal_approved: "client.goal.approved",
137
147
  goal_rejected: "client.goal.rejected",
138
148
  goal_share_requested: "client.goal.share_requested",
149
+ goals_share_requested: "client.goals.share_requested",
150
+ profile_correction_requested: "client.profile.correction_requested",
151
+ ucc_update_pending: "client.profile.ucc_update_pending",
139
152
  goal_shared: "client.goal.shared",
140
153
  goal_share_revoked: "client.goal.revoked",
141
154
  relationship_attached: "client.relationship.attached",
@@ -172,6 +185,7 @@ export const CLIENT_PROFILE_SETTING_DESCRIPTIONS = Object.freeze({
172
185
  "modes.goal_approval": "Hold partner-added goals until the client approves them.",
173
186
  "modes.resolve_by_pan": "Show an existing client instead of a duplicate error when a PAN is found.",
174
187
  "modes.mf_one_investor": "Refuse a second MF investor for a person who already has one (D9).",
188
+ "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).",
175
189
  "modes.linked_visibility": "Clients reached only through a secondary mapping or a linked user appear in lists, as the limited card.",
176
190
  corporate_distributor_types: "distributors.distributor_type values whose clients get a corporate profile.",
177
191
  house_distributor_codes: "Distributor codes treated as direct (InvestValue's own house distributors).",
@@ -199,6 +213,8 @@ export const CLIENT_PROFILE_SETTING_DESCRIPTIONS = Object.freeze({
199
213
  "field_lock.lock_requires_kyc_verified": "The personal lock starts only once KYC is verified.",
200
214
  "field_lock.lock_requires_ucc": "The personal lock starts only once a UCC exists.",
201
215
  "change_request.ttl_minutes": "Minutes a corporate change request waits for the client's OTP.",
216
+ "corrections.fields": "Details a client asks their RM to correct (WhatsApp + email to the RM), never edits in the app.",
217
+ "ucc_followup.product_code": "Product whose UCC field rules (uccfieldrules) decide which client edits flag the profile for a UCC update.",
202
218
  "goals.partner_goal_requires_approval": "Goals added by partners or employees wait for the client's approval.",
203
219
  "goals.corporate_goals_stay_with_corporate": "Corporate-profile goals are never shown to main-profile partners.",
204
220
  "events.profile_change_requested": "Event name: a corporate change was sent to the client.",
@@ -209,6 +225,9 @@ export const CLIENT_PROFILE_SETTING_DESCRIPTIONS = Object.freeze({
209
225
  "events.goal_approved": "Event name: the client approved a goal.",
210
226
  "events.goal_rejected": "Event name: the client rejected a goal.",
211
227
  "events.goal_share_requested": "Event name: a partner asked to see a goal.",
228
+ "events.profile_correction_requested": "Event name: a client asked their RM to correct a detail they cannot edit.",
229
+ "events.ucc_update_pending": "Event name: a client changed UCC details in the app; backoffice / the RM must run the UCC update.",
230
+ "events.goals_share_requested": "Event name: a partner asked the client to share their goals (no goal named — R16).",
212
231
  "events.goal_shared": "Event name: the client shared a goal with a partner.",
213
232
  "events.goal_share_revoked": "Event name: the client stopped sharing a goal.",
214
233
  "events.relationship_attached": "Event name: a new distributor or employee was linked to a client.",
@@ -0,0 +1,45 @@
1
+ /**
2
+ * UCC follow-up — which of a client's own app edits leave their UCC (BSE) out of date.
3
+ *
4
+ * A client never pushes to BSE from the app. When they change a detail the product's UCC field
5
+ * rules (`uccfieldrules`, one document per product) mark as synced to the UCC, the profile is
6
+ * flagged instead, and backoffice or the RM runs the UCC update from the web.
7
+ *
8
+ * Pure: the backend loads the rules document and applies the result.
9
+ */
10
+
11
+ /**
12
+ * The rules name a client's contact lists by their UCC meaning; the client record by its field.
13
+ * A data-model fact, not configuration — the two names never diverge per environment.
14
+ */
15
+ export const UCC_RULE_NAME_OF_CLIENT_FIELD = Object.freeze({
16
+ phone_list: "mobile",
17
+ email_list: "email",
18
+ });
19
+
20
+ /** Rule names a client edit can leave out of date: warn-locked fields synced to the UCC, and
21
+ * collections (bank, nominees) whose tagged entries warn on edit. */
22
+ export function uccSyncedRuleNames(rules) {
23
+ if (!rules || rules.is_active === false) return [];
24
+ const fields = Object.entries(rules.fields ?? {})
25
+ .filter(([, rule]) => rule && rule.lock === "warn" && rule.syncEndpoint)
26
+ .map(([name]) => name);
27
+ const collections = Object.entries(rules.collections ?? {})
28
+ .filter(([, rule]) => rule && rule.edit_tagged === "warn")
29
+ .map(([name]) => name);
30
+ return [...new Set([...fields, ...collections])];
31
+ }
32
+
33
+ /**
34
+ * Of the client fields an edit touched, the ones that leave the UCC out of date — by the rule
35
+ * name backoffice will recognise ("mobile", not "phone_list").
36
+ */
37
+ export function uccFieldsTouched(touchedClientFields, rules) {
38
+ const synced = new Set(uccSyncedRuleNames(rules));
39
+ const out = new Set();
40
+ for (const field of touchedClientFields ?? []) {
41
+ const ruleName = UCC_RULE_NAME_OF_CLIENT_FIELD[field] ?? field;
42
+ if (synced.has(ruleName)) out.add(ruleName);
43
+ }
44
+ return [...out];
45
+ }