@i4e/invest4edu-access-core 0.17.0 → 0.17.2

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.17.0",
3
+ "version": "0.17.2",
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": {
@@ -155,8 +155,20 @@ export function createEntitlementStore({ getDb, toObjectId = (v) => v, logger =
155
155
  subscription, entitlementRows: rows, featureCode, feature, used, credits, identity, bypass, now,
156
156
  });
157
157
 
158
- // Carry what consume() needs, so it cannot recompute the period key and count against a
159
- // different bucket than the one that was just checked.
158
+ /**
159
+ * Carry what consume() needs, so it cannot recompute the period key and count against a
160
+ * different bucket than the one that was just checked.
161
+ *
162
+ * The carried row takes its ceiling from the RESOLVED result, not from the raw plan row —
163
+ * because a per-subject override changes the decision without changing the row it came
164
+ * from. Carrying the raw row meant consume guarded on the plan's quota while the check had
165
+ * already passed on the overridden one: a subject granted 5 was refused at 2, and it
166
+ * surfaced as a counter that had simply stopped moving.
167
+ */
168
+ const effectiveRow = row
169
+ ? { ...row, quota: result.quota, overage_policy: result.overage_policy || row.overage_policy }
170
+ : null;
171
+
160
172
  return {
161
173
  ...result,
162
174
  carriedSubjectType: subjectType,
@@ -164,7 +176,7 @@ export function createEntitlementStore({ getDb, toObjectId = (v) => v, logger =
164
176
  carriedSubscriptionId: subscription._id,
165
177
  carriedPeriodStart: subscription.period_start || null,
166
178
  carriedIdentity: identity,
167
- carriedRow: row,
179
+ carriedRow: effectiveRow,
168
180
  };
169
181
  } catch (e) {
170
182
  logger.warn(`[entitlement] resolve failed for ${featureCode}, allowing through: ${e.message}`);
@@ -93,7 +93,20 @@ export function resolveEntitlementRow(rows = [], featureCode, now = new Date())
93
93
 
94
94
  /** Apply a per-firm override on top of the plan row. Concessions live on the subscription. */
95
95
  function withOverride(row, overrides = [], featureCode) {
96
- const o = (overrides || []).find((x) => x && x.feature_code === featureCode);
96
+ /**
97
+ * Two shapes in the wild, and getting this wrong is silent AND total.
98
+ *
99
+ * Everything that WRITES an override — the model, the admin API, the Subscribers view — uses an
100
+ * object map keyed by feature code. This function only understood an array, so `.find` on the
101
+ * object threw, `resolveEntitlement` failed open, and the subscriber stopped being metered
102
+ * altogether. Setting a per-customer allowance therefore disabled that customer's counting —
103
+ * the opposite of the intent, with no error anywhere.
104
+ *
105
+ * Both shapes are accepted now. The map is the canonical one.
106
+ */
107
+ const o = Array.isArray(overrides)
108
+ ? overrides.find((x) => x && x.feature_code === featureCode)
109
+ : (overrides && typeof overrides === "object" ? overrides[featureCode] : null);
97
110
  if (!o) return row;
98
111
  const merged = { ...row };
99
112
  for (const k of ["unlocked", "quota", "period_granularity", "quota_scope", "overage_policy", "single_usage_price"]) {