@i4e/invest4edu-access-core 0.11.0 → 0.12.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.11.0",
3
+ "version": "0.12.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": {
@@ -16,7 +16,8 @@
16
16
  "./entitlement": "./src/entitlement.js",
17
17
  "./entitlement-schema": "./src/entitlement-schema.js",
18
18
  "./grid-schema": "./src/grid-schema.js",
19
- "./route-features": "./src/route-features.js"
19
+ "./route-features": "./src/route-features.js",
20
+ "./subscription-lifecycle": "./src/subscription-lifecycle.js"
20
21
  },
21
22
  "scripts": {
22
23
  "test": "node --test test/"
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Subscription lifecycle — statuses, transitions and event types. SHARED, because code branches
3
+ * on these in both backends and a status list that drifts between them means one side treats a
4
+ * subscription as live while the other has cut access (invariant #11).
5
+ *
6
+ * The machine:
7
+ *
8
+ * trialing → active | cancelled | expired
9
+ * active → past_due | blocked | cancelled
10
+ * past_due → active | blocked | cancelled (payment recovered | gave up | user quit)
11
+ * blocked → active | cancelled (recovered | closed)
12
+ * cancelled / expired → (terminal — a new purchase creates a NEW subscription)
13
+ *
14
+ * Terminal states stay terminal on purpose: "reactivating" a cancelled row would resurrect its
15
+ * history, overrides and period as if nothing happened. A fresh subscription is honest about the
16
+ * gap and keeps the audit trail of the old one intact.
17
+ *
18
+ * Upgrade/downgrade are TRANSITIONS OF PLAN, not of status — a plan change happens on a live
19
+ * subscription and does not appear here.
20
+ */
21
+
22
+ export const SUBSCRIPTION_STATUSES = Object.freeze([
23
+ "trialing", "active", "past_due", "blocked", "cancelled", "expired",
24
+ ]);
25
+
26
+ /** States in which entitlements resolve. past_due keeps working — cutting access the moment a
27
+ * payment is late loses more than it protects; `blocked` is the deliberate cut-off. */
28
+ export const LIVE_STATUSES = Object.freeze(["trialing", "active", "past_due"]);
29
+
30
+ export const TRANSITIONS = Object.freeze({
31
+ trialing: ["active", "cancelled", "expired"],
32
+ active: ["past_due", "blocked", "cancelled"],
33
+ past_due: ["active", "blocked", "cancelled"],
34
+ blocked: ["active", "cancelled"],
35
+ cancelled: [],
36
+ expired: [],
37
+ });
38
+
39
+ /** `{ ok }` or `{ ok: false, message }` naming the legal moves — the caller shows it verbatim. */
40
+ export function canTransition(from, to) {
41
+ const allowed = TRANSITIONS[from];
42
+ if (!allowed) return { ok: false, message: `${from} is not a subscription status` };
43
+ if (from === to) return { ok: false, message: `already ${from}` };
44
+ if (!allowed.includes(to)) {
45
+ return {
46
+ ok: false,
47
+ message: allowed.length
48
+ ? `${from} can only move to: ${allowed.join(", ")}`
49
+ : `${from} is terminal — a new purchase creates a new subscription`,
50
+ };
51
+ }
52
+ return { ok: true };
53
+ }
54
+
55
+ /**
56
+ * Event types — the audit spine AND what the Events Engine receives (`subscription.<type>`).
57
+ * Engagement and monitoring both hang off this list, so an unlisted type is an event nobody can
58
+ * subscribe to: recording one is refused rather than silently accepted.
59
+ */
60
+ export const SUBSCRIPTION_EVENT_TYPES = Object.freeze([
61
+ "created", "trial_started", "activated", "renewed",
62
+ "plan_changed", "cycle_changed", "period_extended",
63
+ "override_set", "override_removed",
64
+ "status_changed", "cancelled", "blocked", "expired",
65
+ "payment_captured", "payment_failed",
66
+ "quota_exceeded",
67
+ ]);
68
+
69
+ export default { SUBSCRIPTION_STATUSES, LIVE_STATUSES, TRANSITIONS, canTransition, SUBSCRIPTION_EVENT_TYPES };