@i4e/invest4edu-access-core 0.22.0 → 0.24.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.js +35 -0
- package/src/subscription-lifecycle.js +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@i4e/invest4edu-access-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.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.js
CHANGED
|
@@ -171,7 +171,42 @@ export function resolveEntitlement({
|
|
|
171
171
|
// not silently switch features off for everyone the moment it ships.
|
|
172
172
|
if (!row) return open(R.NOT_IN_PLAN);
|
|
173
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Locked: the plan does not carry this feature at all.
|
|
176
|
+
*
|
|
177
|
+
* A locked row is an ACCESS decision, so no allowance gets past it — that is what makes tiering
|
|
178
|
+
* mean anything. If a shared credit balance could unlock a locked feature, the cheap plan plus a
|
|
179
|
+
* top-up would be strictly better than the expensive plan, and the tier would collapse.
|
|
180
|
+
*
|
|
181
|
+
* A unit bought OUTRIGHT is different, and is the deliberate escape hatch: someone who paid for
|
|
182
|
+
* one portfolio analysis has bought that analysis, not access to the feature. `credits` here is
|
|
183
|
+
* always the balance for THIS feature — the caller resolves credits-mode plans down this same
|
|
184
|
+
* path precisely because their shared pool must not reach it — so consulting it cannot leak the
|
|
185
|
+
* pool. Nothing is on sale individually by default, which keeps this dormant until someone
|
|
186
|
+
* prices a service for it.
|
|
187
|
+
*/
|
|
174
188
|
if (row.unlocked === false) {
|
|
189
|
+
const owned = Math.max(0, Number(credits || 0));
|
|
190
|
+
if (owned >= 1) {
|
|
191
|
+
return {
|
|
192
|
+
...base,
|
|
193
|
+
unlocked: true,
|
|
194
|
+
allowed: true,
|
|
195
|
+
credits: owned,
|
|
196
|
+
remaining: owned,
|
|
197
|
+
usingCredit: true,
|
|
198
|
+
metered: true,
|
|
199
|
+
quota: 0,
|
|
200
|
+
used: Number(used || 0),
|
|
201
|
+
overage_policy: row.overage_policy || "block",
|
|
202
|
+
reason: R.OK,
|
|
203
|
+
period: periodKey(row.period_granularity || "month", {
|
|
204
|
+
now, subscriptionId: subscription._id, periodStart: subscription.period_start,
|
|
205
|
+
}),
|
|
206
|
+
subject_key: subjectKeyFor(row.quota_scope, identity),
|
|
207
|
+
cost: 1,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
175
210
|
return { ...base, unlocked: false, allowed: false, overage_policy: row.overage_policy, reason: R.LOCKED };
|
|
176
211
|
}
|
|
177
212
|
|
|
@@ -70,6 +70,12 @@ export const SUBSCRIPTION_EVENT_TYPES = Object.freeze([
|
|
|
70
70
|
// Consumption that happened outside the software — a webinar attended, a VPD session held.
|
|
71
71
|
// There is no request behind it, so this event is the only record of who said it happened.
|
|
72
72
|
"usage_recorded",
|
|
73
|
+
/**
|
|
74
|
+
* A term closed with credits unspent and the plan's policy carried them forward. Worth its own
|
|
75
|
+
* type because a customer WILL ask why their balance is bigger than their allowance, and the
|
|
76
|
+
* answer — which period it came from, how much, and what the cap did — has to be findable.
|
|
77
|
+
*/
|
|
78
|
+
"credits_rolled_over",
|
|
73
79
|
/**
|
|
74
80
|
* The refund conversation, recorded as it happens rather than reconstructed afterwards.
|
|
75
81
|
*
|