@i4e/invest4edu-access-core 0.29.0 → 0.31.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 +1 -1
- package/src/entitlement-store.js +42 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@i4e/invest4edu-access-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"description": "Shared access-control primitives for NeoFindesk: tenant keystone, role capabilities, reportee tree, feature flags, and the unified access engine (registry schema, snapshot resolver, visibleWhen).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
package/src/entitlement-store.js
CHANGED
|
@@ -157,7 +157,28 @@ export function createEntitlementStore({
|
|
|
157
157
|
* The per-plan override is your "premium plans give more usage" lever: eCAS can cost 2 on
|
|
158
158
|
* Discover and 1 on Grow without any per-feature matrix returning through the back door.
|
|
159
159
|
*/
|
|
160
|
-
|
|
160
|
+
/**
|
|
161
|
+
* A per-feature BUDGET opts out of credits mode.
|
|
162
|
+
*
|
|
163
|
+
* `quota` is overloaded, and the two meanings need opposite handling: on VPD it is "12
|
|
164
|
+
* sessions a year" — a budget that must decrement and refuse at zero — while on stock tips it
|
|
165
|
+
* is "5 ideas per answer", a ceiling read on every request and never spent. Credits mode
|
|
166
|
+
* counts only the wallet, so a budget expressed that way silently never moved: Grow showed
|
|
167
|
+
* 12 VPD sessions that could be used forever.
|
|
168
|
+
*
|
|
169
|
+
* So a row with a real number, on a feature that does NOT shape a response, falls through to
|
|
170
|
+
* count mode and is governed by its own counter. Credits stay for what is priced in credits.
|
|
171
|
+
* `shapes_response` is the discriminator because it is a property of the FEATURE — whether it
|
|
172
|
+
* trims an answer or counts a use — not of the plan that sells it.
|
|
173
|
+
*/
|
|
174
|
+
const budgetRow = creditsMode ? resolveEntitlementRow(rows, featureCode, now) : null;
|
|
175
|
+
const isOwnBudget = !!budgetRow
|
|
176
|
+
&& budgetRow.unlocked !== false
|
|
177
|
+
&& feature.shapes_response !== true
|
|
178
|
+
&& budgetRow.quota !== null && budgetRow.quota !== undefined && budgetRow.quota !== ""
|
|
179
|
+
&& Number.isFinite(Number(budgetRow.quota));
|
|
180
|
+
|
|
181
|
+
if (creditsMode && !isOwnBudget) {
|
|
161
182
|
const svcRow = resolveEntitlementRow(rows, featureCode, now);
|
|
162
183
|
// Not named in a credits plan → the same fail-open NOT_IN_PLAN as count mode. A plan
|
|
163
184
|
// that forgot to include a feature is a config gap, not a paywall.
|
|
@@ -210,8 +231,28 @@ export function createEntitlementStore({
|
|
|
210
231
|
? { ...creditRows[0], quota: result.quota, overage_policy: result.overage_policy || creditRows[0].overage_policy }
|
|
211
232
|
: null;
|
|
212
233
|
|
|
234
|
+
/**
|
|
235
|
+
* A CEILING survives credits mode.
|
|
236
|
+
*
|
|
237
|
+
* `limit` shapes a response — "your list shows 5" — and is read on every request without
|
|
238
|
+
* ever being spent. A PRICE is what a use costs. They are orthogonal, and the two live on
|
|
239
|
+
* different fields for exactly that reason.
|
|
240
|
+
*
|
|
241
|
+
* Without this line the credits branch returned the wallet's numbers and no `limit` at
|
|
242
|
+
* all, so a consumer reading `ent.limit` (NFD AI's stock-tips shaper does) saw nothing
|
|
243
|
+
* and applied no ceiling. A free plan promising five stock ideas silently served every
|
|
244
|
+
* one of them — the paywall looked configured and was not.
|
|
245
|
+
*
|
|
246
|
+
* Same UNSET-vs-0 care as count mode: null means "no ceiling", 0 means "show nothing".
|
|
247
|
+
*/
|
|
248
|
+
const rawCeiling = svcRow.quota;
|
|
249
|
+
const ceiling = rawCeiling === null || rawCeiling === undefined || rawCeiling === ""
|
|
250
|
+
? null
|
|
251
|
+
: Number(rawCeiling);
|
|
252
|
+
|
|
213
253
|
return {
|
|
214
254
|
...result,
|
|
255
|
+
limit: ceiling !== null && Number.isFinite(ceiling) && ceiling >= 0 ? ceiling : null,
|
|
215
256
|
mode: "credits",
|
|
216
257
|
carriedSubjectType: subjectType,
|
|
217
258
|
carriedSubjectId: subjectId,
|