@i4e/invest4edu-access-core 0.39.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.39.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";
@@ -124,6 +124,15 @@ export const DEFAULT_CLIENT_PROFILE_SETTINGS = deepFreeze({
124
124
  change_request: {
125
125
  ttl_minutes: 10,
126
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
+ },
127
136
  goals: {
128
137
  partner_goal_requires_approval: true,
129
138
  corporate_goals_stay_with_corporate: true,
@@ -138,6 +147,8 @@ export const DEFAULT_CLIENT_PROFILE_SETTINGS = deepFreeze({
138
147
  goal_rejected: "client.goal.rejected",
139
148
  goal_share_requested: "client.goal.share_requested",
140
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",
141
152
  goal_shared: "client.goal.shared",
142
153
  goal_share_revoked: "client.goal.revoked",
143
154
  relationship_attached: "client.relationship.attached",
@@ -202,6 +213,8 @@ export const CLIENT_PROFILE_SETTING_DESCRIPTIONS = Object.freeze({
202
213
  "field_lock.lock_requires_kyc_verified": "The personal lock starts only once KYC is verified.",
203
214
  "field_lock.lock_requires_ucc": "The personal lock starts only once a UCC exists.",
204
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.",
205
218
  "goals.partner_goal_requires_approval": "Goals added by partners or employees wait for the client's approval.",
206
219
  "goals.corporate_goals_stay_with_corporate": "Corporate-profile goals are never shown to main-profile partners.",
207
220
  "events.profile_change_requested": "Event name: a corporate change was sent to the client.",
@@ -212,6 +225,8 @@ export const CLIENT_PROFILE_SETTING_DESCRIPTIONS = Object.freeze({
212
225
  "events.goal_approved": "Event name: the client approved a goal.",
213
226
  "events.goal_rejected": "Event name: the client rejected a goal.",
214
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.",
215
230
  "events.goals_share_requested": "Event name: a partner asked the client to share their goals (no goal named — R16).",
216
231
  "events.goal_shared": "Event name: the client shared a goal with a partner.",
217
232
  "events.goal_share_revoked": "Event name: the client stopped sharing a goal.",
@@ -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
+ }