@i4e/invest4edu-access-core 0.34.0 → 0.36.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.34.0",
3
+ "version": "0.36.0",
4
4
  "description": "Shared access-control primitives for NeoFindesk: tenant keystone, role capabilities, reportee tree, entity/role vocabulary, feature flags, and the unified access engine (registry schema, snapshot resolver, visibleWhen).",
5
5
  "type": "module",
6
6
  "exports": {
@@ -58,6 +58,17 @@ export const AUDIENCE = Object.freeze({
58
58
  */
59
59
  export const SECTION_KEYS = Object.freeze([
60
60
  "actions", // the 3-step action plan / recommendations
61
+ /*
62
+ * The per-holding VERDICT — a fund's Buy/Hold/Watch/Exit chip and a stock's signal.
63
+ *
64
+ * Separate from `actions` because they are withheld for the same reason but live in different
65
+ * places: `actions` is one panel, this is a chip on every fund row and every stock row across
66
+ * several tabs. A client who cannot see the action plan but can see "EXIT" against a fund has
67
+ * been given the recommendation anyway, just without the reasoning.
68
+ *
69
+ * Covers BOTH vocabularies — funds carry `action`, stocks carry `signal`.
70
+ */
71
+ "fundActions",
61
72
  "phc", // portfolio health-check detail body
62
73
  "chat", // AI chat
63
74
  "performance", // performance / gains
@@ -70,6 +81,7 @@ export const SECTION_KEYS = Object.freeze([
70
81
  /** Everything on. The answer whenever we cannot prove a narrower one is correct. */
71
82
  export const ALL_VISIBLE = Object.freeze({
72
83
  actions: true,
84
+ fundActions: true,
73
85
  phc: true,
74
86
  chat: true,
75
87
  performance: true,
@@ -81,17 +93,36 @@ export const ALL_VISIBLE = Object.freeze({
81
93
  tone: "adviser",
82
94
  });
83
95
 
84
- /** The reduced view. Partner and RM-E1 clients get exactly this; direct adds the CTA. */
96
+ /**
97
+ * The reduced view — REVISED 21 Sep 2026.
98
+ *
99
+ * It used to withhold almost the whole report. It now withholds exactly the RECOMMENDATION and
100
+ * nothing else, because the reason for withholding was never "this client should see less": it is
101
+ * that an employee services them and that conversation belongs to the RM. Everything the client
102
+ * can read for themselves — the health detail, performance, fund analysis, the score breakdown,
103
+ * per-fund detail — tells them where they stand without telling them what to do.
104
+ *
105
+ * Two keys carry it, and both are needed: the action plan is one panel, but the per-holding
106
+ * verdict is a chip on every fund and stock row. Withholding the panel alone would hand over the
107
+ * recommendation anyway, minus the reasoning.
108
+ *
109
+ * `chat` stays locked for the same reason, and it is the subtle one: a client can simply ASK the
110
+ * chat whether to sell a fund. Leaving it open would route around both keys above.
111
+ *
112
+ * `tone: b2c` stays: the narrative still speaks to a client rather than an adviser, and it must
113
+ * keep describing rather than prescribing now that it is the main thing they read.
114
+ */
85
115
  const REDUCED = Object.freeze({
86
116
  actions: false,
87
- phc: false,
117
+ fundActions: false,
88
118
  chat: false,
89
- performance: false,
90
- keyInsights: false,
91
- fundAnalysis: false,
92
- scoreBreakdownFunds: false,
119
+ phc: true,
120
+ performance: true,
121
+ keyInsights: true,
122
+ fundAnalysis: true,
123
+ scoreBreakdownFunds: true,
93
124
  advisorCta: false,
94
- fundDetail: "minimal",
125
+ fundDetail: "full",
95
126
  tone: "b2c",
96
127
  });
97
128
 
@@ -103,17 +134,32 @@ const REDUCED = Object.freeze({
103
134
  export const AUDIENCE_DEFAULTS = Object.freeze({
104
135
  [AUDIENCE.FULL]: ALL_VISIBLE,
105
136
 
106
- // "Don't show for Partner clients & RM clients where RM is E1. Actions will not come.
107
- // Portfolio Health Score component — don't show; only show Health Score, no recommendations.
108
- // Hide the AI chatbot. Remove Key / Fund Analysis. Hide Performance. Less fund details.
109
- // Score breakdown — remove underlying fund. Language B2C / neutral."
110
- [AUDIENCE.PARTNER]: REDUCED,
137
+ /*
138
+ * REVISED 12 Sep 2026 (Peeyush). The first cut reduced all three audiences and differed only by
139
+ * a CTA. It now reduces ONE.
140
+ *
141
+ * partner full — unlocking a partner's client is out of scope, so gating them would leave a
142
+ * reduced report with no way to ever open it.
143
+ * direct full — a direct client has no partner and no servicing RM, so there is nobody for
144
+ * "ask your advisor" to point at.
145
+ * rm_e1 reduced — an employee services this client, so the withheld sections have an owner
146
+ * who can walk the client through them.
147
+ *
148
+ * Measured on prod the day this changed: 13,327 partner, 2,121 e1, 232 direct of 15,680. So the
149
+ * feature applies to ~13.5% of the book and 86.5% is untouched.
150
+ */
151
+ [AUDIENCE.PARTNER]: ALL_VISIBLE,
111
152
 
112
- // Same treatment as a partner client — the review grouped them in one line.
113
- [AUDIENCE.RM_E1]: REDUCED,
153
+ // "Don't show for RM clients where RM is E1. Actions will not come. Portfolio Health Score
154
+ // component — don't show; only show Health Score, no recommendations. Hide the AI chatbot.
155
+ // Remove Key / Fund Analysis. Hide Performance. Less fund details. Score breakdown — remove
156
+ // underlying fund. Language B2C / neutral."
157
+ //
158
+ // The CTA rides HERE rather than on `direct`, inverting the first cut: this is the audience
159
+ // with an adviser to talk to, and the one whose sections are withheld.
160
+ [AUDIENCE.RM_E1]: Object.freeze({ ...REDUCED, advisorCta: true }),
114
161
 
115
- // "Direct Clients — call advisor to get details to know the plan. Same storytelling."
116
- [AUDIENCE.DIRECT]: Object.freeze({ ...REDUCED, advisorCta: true }),
162
+ [AUDIENCE.DIRECT]: ALL_VISIBLE,
117
163
  });
118
164
 
119
165
  const E1_TYPE_DISTRIBUTOR = "distributor";