@i4e/invest4edu-access-core 0.30.0 → 0.32.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.
@@ -1,194 +1,194 @@
1
- /**
2
- * Entitlement / subscription schema DEFINITIONS — @i4e/invest4edu-access-core (P6).
3
- *
4
- * Plain objects like access-schema.js (D4: no Mongoose in the package). Each backend registers its
5
- * own models from these, so field names cannot drift between v1 and v2.
6
- *
7
- * RBAC answers "may they?"; entitlement answers "is it unlocked, and are they in quota?". Both must
8
- * pass. See TDD §6, and §6.2a for the 2026-07-29 amendment that generalised quota period + subject
9
- * and added the service-facing meter.
10
- *
11
- * Design: nfd-ui-nextjs/IV-CodingAgent/specs/access-management/TDD-unified-access-and-subscription.md
12
- */
13
-
14
- /** How often a quota allowance resets. Carried in the period key, so no cron is needed. */
15
- export const PERIOD_GRANULARITIES = Object.freeze(["day", "month", "year", "term", "lifetime"]);
16
-
17
- /** Whose allowance is being spent. `account` = per firm (the legacy assumption); `user` = per seat. */
18
- export const QUOTA_SCOPES = Object.freeze(["account", "user"]);
19
-
20
- /** What happens when the allowance runs out. */
21
- export const OVERAGE_POLICIES = Object.freeze(["block", "single_usage", "unlimited"]);
22
-
23
- export const SUBSCRIPTION_STATUSES = Object.freeze([
24
- "trialing", "active", "past_due", "blocked", "cancelled", "expired",
25
- ]);
26
-
27
- /**
28
- * The timezone every calendar boundary is evaluated in.
29
- *
30
- * A daily quota is meaningless without one: in UTC an Indian partner's "10 a day" would reset at
31
- * 05:30 local, so they would lose the tail of every working day. Matches the `tz` the MIS pipelines
32
- * already use.
33
- */
34
- export const QUOTA_TIMEZONE = "Asia/Kolkata";
35
-
36
- /**
37
- * The synthetic feature that carries a credits-mode plan's allowance.
38
- *
39
- * Credits mode needed no new machinery precisely because the allowance is an ordinary
40
- * entitlement row on this code: it inherits cycle columns, effective-date versioning, trial
41
- * precedence, as-of resolution and the race-free counter. A plan IS credits-mode when a row for
42
- * this code resolves — there is no separate flag to drift out of step with the data.
43
- */
44
- export const CREDITS_FEATURE_CODE = "SUBSCRIPTION.CREDITS";
45
-
46
- /** Reasons an entitlement decision can carry. Stable strings — they surface in APIs and logs. */
47
- export const ENTITLEMENT_REASONS = Object.freeze({
48
- OK: "ok",
49
- NO_SUBSCRIPTION: "no_subscription",
50
- NOT_IN_PLAN: "not_in_plan",
51
- LOCKED: "locked",
52
- QUOTA_EXCEEDED: "quota_exceeded",
53
- SUBSCRIPTION_BLOCKED: "subscription_blocked",
54
- ADMIN_BYPASS: "admin_bypass",
55
- NOT_METERED: "not_metered",
56
- ENGINE_OFF: "engine_off",
57
- RESOLVE_FAILED: "resolve_failed",
58
- });
59
-
60
- /** Global: the plans on sale. */
61
- export const PLAN_CATALOG_DEF = Object.freeze({
62
- collection: "plan_catalog",
63
- fields: Object.freeze({
64
- plan_code: { type: "String", required: true, unique: true, index: true, immutable: true },
65
- name: { type: "String", required: true },
66
- description: { type: "String" },
67
- billing_cycle: { type: "String", enum: ["monthly", "quarterly", "annual"], default: "annual" },
68
- price: { type: "Number", default: 0 },
69
- currency: { type: "String", default: "INR" },
70
- position: { type: "Number", default: 0 },
71
- status: { type: "String", enum: ["active", "dormant", "retired"], default: "active" },
72
- }),
73
- });
74
-
75
- /**
76
- * Global: `(plan, cycle, feature)` → what that plan grants for that feature.
77
- *
78
- * VERSIONED by `effective_date`: resolve the row with the greatest `effective_date <= now`. Prices
79
- * and quotas change, and a past invoice must remain explicable — so rows are superseded, never
80
- * edited in place.
81
- */
82
- export const PLAN_ENTITLEMENT_DEF = Object.freeze({
83
- collection: "plan_entitlements",
84
- fields: Object.freeze({
85
- plan_code: { type: "String", required: true, index: true },
86
- billing_cycle: { type: "String", required: true },
87
- feature_code: { type: "String", required: true, index: true },
88
-
89
- unlocked: { type: "Boolean", default: true },
90
- // null = unlimited. 0 is a real value: "in the plan, but no allowance".
91
- quota: { type: "Number", default: null },
92
- period_granularity: { type: "String", enum: PERIOD_GRANULARITIES, default: "month" },
93
- quota_scope: { type: "String", enum: QUOTA_SCOPES, default: "account" },
94
- overage_policy: { type: "String", enum: OVERAGE_POLICIES, default: "block" },
95
- single_usage_price: { type: "Number", default: null },
96
-
97
- effective_date: { type: "Date", required: true, index: true },
98
- superseded_date: { type: "Date", default: null },
99
- }),
100
- indexes: Object.freeze([
101
- { keys: { plan_code: 1, billing_cycle: 1, feature_code: 1, effective_date: -1 } },
102
- ]),
103
- });
104
-
105
- /** Tenant: one subscription per firm. Replaces AccountPlan + distributor.selected_plan. */
106
- export const ACCOUNT_SUBSCRIPTION_DEF = Object.freeze({
107
- collection: "account_subscriptions",
108
- fields: Object.freeze({
109
- account_id: { type: "ObjectId", required: true, index: true },
110
- plan_code: { type: "String", required: true },
111
- billing_cycle: { type: "String", required: true },
112
- status: { type: "String", enum: SUBSCRIPTION_STATUSES, default: "trialing", index: true },
113
- start_date: { type: "Date", required: true },
114
- end_date: { type: "Date", default: null },
115
- is_trial: { type: "Boolean", default: false },
116
- trial_ends_at: { type: "Date", default: null },
117
- // Per-firm overrides of the plan row: [{ feature_code, quota, unlocked, … }]. Sales concessions
118
- // belong here, NOT as an edit to the shared plan row.
119
- entitlement_overrides: { type: "[Mixed]", default: [] },
120
- }),
121
- });
122
-
123
- /**
124
- * Tenant: the quota counters. MUST live in the app DB — `consume` has to be atomic with the read,
125
- * so this cannot be a side/analytics store.
126
- *
127
- * `subject_key` is the account id or the user id depending on `quota_scope`, so one collection
128
- * serves both without a second shape.
129
- */
130
- export const SUBSCRIPTION_USAGE_DEF = Object.freeze({
131
- collection: "subscription_usage",
132
- fields: Object.freeze({
133
- subscription_id: { type: "ObjectId", required: true, index: true },
134
- subject_key: { type: "String", required: true },
135
- feature_code: { type: "String", required: true },
136
- period: { type: "String", required: true },
137
- used: { type: "Number", default: 0 },
138
- last_consumed_at: { type: "Date", default: null },
139
- }),
140
- indexes: Object.freeze([
141
- { keys: { subscription_id: 1, subject_key: 1, feature_code: 1, period: 1 }, options: { unique: true } },
142
- ]),
143
- });
144
-
145
- /**
146
- * Tenant: the exactly-once ledger for `consume`.
147
- *
148
- * A tool that times out and retries must not charge the partner twice, so every consume carries an
149
- * idempotency key and the unique index — not application logic — is what enforces it.
150
- */
151
- export const USAGE_IDEMPOTENCY_DEF = Object.freeze({
152
- collection: "usage_idempotency",
153
- fields: Object.freeze({
154
- idempotency_key: { type: "String", required: true, unique: true, index: true },
155
- account_id: { type: "ObjectId", index: true },
156
- feature_code: { type: "String", required: true },
157
- subject_key: { type: "String" },
158
- period: { type: "String" },
159
- n: { type: "Number", default: 1 },
160
- // The decision returned the first time, replayed verbatim on a duplicate key.
161
- result: { type: "Mixed", default: null },
162
- created_date: { type: "Date", default: "now" },
163
- }),
164
- });
165
-
166
- /** Global: the service callers allowed to meter, and which features each may meter. */
167
- export const METER_CLIENT_DEF = Object.freeze({
168
- collection: "meter_clients",
169
- fields: Object.freeze({
170
- client_code: { type: "String", required: true, unique: true, index: true },
171
- name: { type: "String", required: true },
172
- // Key Vault SECRET NAME, never the secret. An admin API must never be able to read it back.
173
- key_vault_ref: { type: "String", required: true },
174
- // A tool may only meter what it is registered for — a leaked NFD-AI key cannot spend PFA quota.
175
- allowed_features: { type: "[String]", default: [] },
176
- status: { type: "String", enum: ["active", "disabled"], default: "active" },
177
- }),
178
- });
179
-
180
- export default {
181
- PERIOD_GRANULARITIES,
182
- CREDITS_FEATURE_CODE,
183
- QUOTA_SCOPES,
184
- OVERAGE_POLICIES,
185
- SUBSCRIPTION_STATUSES,
186
- QUOTA_TIMEZONE,
187
- ENTITLEMENT_REASONS,
188
- PLAN_CATALOG_DEF,
189
- PLAN_ENTITLEMENT_DEF,
190
- ACCOUNT_SUBSCRIPTION_DEF,
191
- SUBSCRIPTION_USAGE_DEF,
192
- USAGE_IDEMPOTENCY_DEF,
193
- METER_CLIENT_DEF,
194
- };
1
+ /**
2
+ * Entitlement / subscription schema DEFINITIONS — @i4e/invest4edu-access-core (P6).
3
+ *
4
+ * Plain objects like access-schema.js (D4: no Mongoose in the package). Each backend registers its
5
+ * own models from these, so field names cannot drift between v1 and v2.
6
+ *
7
+ * RBAC answers "may they?"; entitlement answers "is it unlocked, and are they in quota?". Both must
8
+ * pass. See TDD §6, and §6.2a for the 2026-07-29 amendment that generalised quota period + subject
9
+ * and added the service-facing meter.
10
+ *
11
+ * Design: nfd-ui-nextjs/IV-CodingAgent/specs/access-management/TDD-unified-access-and-subscription.md
12
+ */
13
+
14
+ /** How often a quota allowance resets. Carried in the period key, so no cron is needed. */
15
+ export const PERIOD_GRANULARITIES = Object.freeze(["day", "month", "year", "term", "lifetime"]);
16
+
17
+ /** Whose allowance is being spent. `account` = per firm (the legacy assumption); `user` = per seat. */
18
+ export const QUOTA_SCOPES = Object.freeze(["account", "user"]);
19
+
20
+ /** What happens when the allowance runs out. */
21
+ export const OVERAGE_POLICIES = Object.freeze(["block", "single_usage", "unlimited"]);
22
+
23
+ export const SUBSCRIPTION_STATUSES = Object.freeze([
24
+ "trialing", "active", "past_due", "blocked", "cancelled", "expired",
25
+ ]);
26
+
27
+ /**
28
+ * The timezone every calendar boundary is evaluated in.
29
+ *
30
+ * A daily quota is meaningless without one: in UTC an Indian partner's "10 a day" would reset at
31
+ * 05:30 local, so they would lose the tail of every working day. Matches the `tz` the MIS pipelines
32
+ * already use.
33
+ */
34
+ export const QUOTA_TIMEZONE = "Asia/Kolkata";
35
+
36
+ /**
37
+ * The synthetic feature that carries a credits-mode plan's allowance.
38
+ *
39
+ * Credits mode needed no new machinery precisely because the allowance is an ordinary
40
+ * entitlement row on this code: it inherits cycle columns, effective-date versioning, trial
41
+ * precedence, as-of resolution and the race-free counter. A plan IS credits-mode when a row for
42
+ * this code resolves — there is no separate flag to drift out of step with the data.
43
+ */
44
+ export const CREDITS_FEATURE_CODE = "SUBSCRIPTION.CREDITS";
45
+
46
+ /** Reasons an entitlement decision can carry. Stable strings — they surface in APIs and logs. */
47
+ export const ENTITLEMENT_REASONS = Object.freeze({
48
+ OK: "ok",
49
+ NO_SUBSCRIPTION: "no_subscription",
50
+ NOT_IN_PLAN: "not_in_plan",
51
+ LOCKED: "locked",
52
+ QUOTA_EXCEEDED: "quota_exceeded",
53
+ SUBSCRIPTION_BLOCKED: "subscription_blocked",
54
+ ADMIN_BYPASS: "admin_bypass",
55
+ NOT_METERED: "not_metered",
56
+ ENGINE_OFF: "engine_off",
57
+ RESOLVE_FAILED: "resolve_failed",
58
+ });
59
+
60
+ /** Global: the plans on sale. */
61
+ export const PLAN_CATALOG_DEF = Object.freeze({
62
+ collection: "plan_catalog",
63
+ fields: Object.freeze({
64
+ plan_code: { type: "String", required: true, unique: true, index: true, immutable: true },
65
+ name: { type: "String", required: true },
66
+ description: { type: "String" },
67
+ billing_cycle: { type: "String", enum: ["monthly", "quarterly", "annual"], default: "annual" },
68
+ price: { type: "Number", default: 0 },
69
+ currency: { type: "String", default: "INR" },
70
+ position: { type: "Number", default: 0 },
71
+ status: { type: "String", enum: ["active", "dormant", "retired"], default: "active" },
72
+ }),
73
+ });
74
+
75
+ /**
76
+ * Global: `(plan, cycle, feature)` → what that plan grants for that feature.
77
+ *
78
+ * VERSIONED by `effective_date`: resolve the row with the greatest `effective_date <= now`. Prices
79
+ * and quotas change, and a past invoice must remain explicable — so rows are superseded, never
80
+ * edited in place.
81
+ */
82
+ export const PLAN_ENTITLEMENT_DEF = Object.freeze({
83
+ collection: "plan_entitlements",
84
+ fields: Object.freeze({
85
+ plan_code: { type: "String", required: true, index: true },
86
+ billing_cycle: { type: "String", required: true },
87
+ feature_code: { type: "String", required: true, index: true },
88
+
89
+ unlocked: { type: "Boolean", default: true },
90
+ // null = unlimited. 0 is a real value: "in the plan, but no allowance".
91
+ quota: { type: "Number", default: null },
92
+ period_granularity: { type: "String", enum: PERIOD_GRANULARITIES, default: "month" },
93
+ quota_scope: { type: "String", enum: QUOTA_SCOPES, default: "account" },
94
+ overage_policy: { type: "String", enum: OVERAGE_POLICIES, default: "block" },
95
+ single_usage_price: { type: "Number", default: null },
96
+
97
+ effective_date: { type: "Date", required: true, index: true },
98
+ superseded_date: { type: "Date", default: null },
99
+ }),
100
+ indexes: Object.freeze([
101
+ { keys: { plan_code: 1, billing_cycle: 1, feature_code: 1, effective_date: -1 } },
102
+ ]),
103
+ });
104
+
105
+ /** Tenant: one subscription per firm. Replaces AccountPlan + distributor.selected_plan. */
106
+ export const ACCOUNT_SUBSCRIPTION_DEF = Object.freeze({
107
+ collection: "account_subscriptions",
108
+ fields: Object.freeze({
109
+ account_id: { type: "ObjectId", required: true, index: true },
110
+ plan_code: { type: "String", required: true },
111
+ billing_cycle: { type: "String", required: true },
112
+ status: { type: "String", enum: SUBSCRIPTION_STATUSES, default: "trialing", index: true },
113
+ start_date: { type: "Date", required: true },
114
+ end_date: { type: "Date", default: null },
115
+ is_trial: { type: "Boolean", default: false },
116
+ trial_ends_at: { type: "Date", default: null },
117
+ // Per-firm overrides of the plan row: [{ feature_code, quota, unlocked, … }]. Sales concessions
118
+ // belong here, NOT as an edit to the shared plan row.
119
+ entitlement_overrides: { type: "[Mixed]", default: [] },
120
+ }),
121
+ });
122
+
123
+ /**
124
+ * Tenant: the quota counters. MUST live in the app DB — `consume` has to be atomic with the read,
125
+ * so this cannot be a side/analytics store.
126
+ *
127
+ * `subject_key` is the account id or the user id depending on `quota_scope`, so one collection
128
+ * serves both without a second shape.
129
+ */
130
+ export const SUBSCRIPTION_USAGE_DEF = Object.freeze({
131
+ collection: "subscription_usage",
132
+ fields: Object.freeze({
133
+ subscription_id: { type: "ObjectId", required: true, index: true },
134
+ subject_key: { type: "String", required: true },
135
+ feature_code: { type: "String", required: true },
136
+ period: { type: "String", required: true },
137
+ used: { type: "Number", default: 0 },
138
+ last_consumed_at: { type: "Date", default: null },
139
+ }),
140
+ indexes: Object.freeze([
141
+ { keys: { subscription_id: 1, subject_key: 1, feature_code: 1, period: 1 }, options: { unique: true } },
142
+ ]),
143
+ });
144
+
145
+ /**
146
+ * Tenant: the exactly-once ledger for `consume`.
147
+ *
148
+ * A tool that times out and retries must not charge the partner twice, so every consume carries an
149
+ * idempotency key and the unique index — not application logic — is what enforces it.
150
+ */
151
+ export const USAGE_IDEMPOTENCY_DEF = Object.freeze({
152
+ collection: "usage_idempotency",
153
+ fields: Object.freeze({
154
+ idempotency_key: { type: "String", required: true, unique: true, index: true },
155
+ account_id: { type: "ObjectId", index: true },
156
+ feature_code: { type: "String", required: true },
157
+ subject_key: { type: "String" },
158
+ period: { type: "String" },
159
+ n: { type: "Number", default: 1 },
160
+ // The decision returned the first time, replayed verbatim on a duplicate key.
161
+ result: { type: "Mixed", default: null },
162
+ created_date: { type: "Date", default: "now" },
163
+ }),
164
+ });
165
+
166
+ /** Global: the service callers allowed to meter, and which features each may meter. */
167
+ export const METER_CLIENT_DEF = Object.freeze({
168
+ collection: "meter_clients",
169
+ fields: Object.freeze({
170
+ client_code: { type: "String", required: true, unique: true, index: true },
171
+ name: { type: "String", required: true },
172
+ // Key Vault SECRET NAME, never the secret. An admin API must never be able to read it back.
173
+ key_vault_ref: { type: "String", required: true },
174
+ // A tool may only meter what it is registered for — a leaked NFD-AI key cannot spend PFA quota.
175
+ allowed_features: { type: "[String]", default: [] },
176
+ status: { type: "String", enum: ["active", "disabled"], default: "active" },
177
+ }),
178
+ });
179
+
180
+ export default {
181
+ PERIOD_GRANULARITIES,
182
+ CREDITS_FEATURE_CODE,
183
+ QUOTA_SCOPES,
184
+ OVERAGE_POLICIES,
185
+ SUBSCRIPTION_STATUSES,
186
+ QUOTA_TIMEZONE,
187
+ ENTITLEMENT_REASONS,
188
+ PLAN_CATALOG_DEF,
189
+ PLAN_ENTITLEMENT_DEF,
190
+ ACCOUNT_SUBSCRIPTION_DEF,
191
+ SUBSCRIPTION_USAGE_DEF,
192
+ USAGE_IDEMPOTENCY_DEF,
193
+ METER_CLIENT_DEF,
194
+ };
@@ -1,99 +1,99 @@
1
- /**
2
- * Type declarations for the entitlement store.
3
- *
4
- * The package is plain JavaScript with JSDoc, which is fine for the Node services. NFD AI is
5
- * TypeScript, and without declarations every call through this module widens to `any` — which
6
- * silently removes the type checking from the one place in the codebase that decides whether a
7
- * customer is over quota. Declaring it here rather than in the consumer keeps one definition:
8
- * a copy in each TypeScript service would be a second description of this contract, free to drift
9
- * from the implementation without anything failing.
10
- */
11
-
12
- export interface EntitlementResult {
13
- /** False only on a deliberate refusal — never on an infrastructure failure. */
14
- allowed: boolean;
15
- unlocked: boolean;
16
- /** True when this feature is counted; consume() is a no-op otherwise. */
17
- metered: boolean;
18
- quota: number | null;
19
- used: number;
20
- /** Plan allowance left PLUS unspent à-la-carte credits. */
21
- remaining: number | null;
22
- /** Unspent à-la-carte units bought for this subject and feature. */
23
- credits?: number;
24
- /** True when the plan allowance is gone and the next call spends a purchased credit. */
25
- usingCredit?: boolean;
26
- overage_policy: string;
27
- reason?: string;
28
- period?: string | null;
29
- subject_key?: string | null;
30
- /** Carried so consume() counts against the exact bucket that was just checked. */
31
- carriedSubscriptionId?: unknown;
32
- carriedSubjectType?: string;
33
- carriedSubjectId?: string;
34
- carriedPeriodStart?: Date | string | null;
35
- carriedIdentity?: { accountId?: string | null; userId?: string | null };
36
- carriedRow?: Record<string, unknown> | null;
37
- }
38
-
39
- export interface ConsumeResult {
40
- consumed: boolean;
41
- /** The unit came out of a purchased credit rather than the plan's period counter. */
42
- fromCredit?: boolean;
43
- /** "not_metered" | "quota_exceeded" | "error" */
44
- reason?: string;
45
- }
46
-
47
- export interface EntitlementStore {
48
- findSubscription(args: {
49
- subjectType: string;
50
- subjectId: string;
51
- }): Promise<Record<string, unknown> | null>;
52
-
53
- entitlementFor(args: {
54
- subjectType: string;
55
- subjectId: string;
56
- featureCode: string;
57
- feature?: Record<string, unknown>;
58
- identity?: { accountId?: string | null; userId?: string | null };
59
- bypass?: boolean;
60
- now?: Date;
61
- }): Promise<EntitlementResult>;
62
-
63
- consume(args: {
64
- entitlement?: EntitlementResult;
65
- featureCode: string;
66
- n?: number;
67
- now?: Date;
68
- subscriptionId?: unknown;
69
- row?: Record<string, unknown> | null;
70
- identity?: { accountId?: string | null; userId?: string | null };
71
- periodStart?: Date | string | null;
72
- }): Promise<ConsumeResult>;
73
-
74
- /** Idempotent; returns null when there is no default plan or the write failed. */
75
- provisionDefault(args: {
76
- subjectType: string;
77
- subjectId: string;
78
- accountId?: string | null;
79
- now?: Date;
80
- }): Promise<Record<string, unknown> | null>;
81
- }
82
-
83
- export function createEntitlementStore(opts: {
84
- /**
85
- * The subscription master switch. Returning false makes the whole store inert — every
86
- * entitlement answers OPEN, nothing is counted and no subject is auto-provisioned — so the code
87
- * can ship to production long before the product does. Defaults to the `SUBSCRIPTION_ENABLED`
88
- * environment variable, and therefore to OFF.
89
- */
90
- isEnabled?: () => boolean | Promise<boolean>;
91
- /** Returns a raw MongoDB `Db`, or null/undefined when the connection is not ready. */
92
- getDb: () => unknown;
93
- /** Cast an id the way the caller's driver expects. Defaults to pass-through. */
94
- toObjectId?: (v: string) => unknown;
95
- logger?: { warn: (msg: string) => void };
96
- }): EntitlementStore;
97
-
98
- declare const _default: { createEntitlementStore: typeof createEntitlementStore };
99
- export default _default;
1
+ /**
2
+ * Type declarations for the entitlement store.
3
+ *
4
+ * The package is plain JavaScript with JSDoc, which is fine for the Node services. NFD AI is
5
+ * TypeScript, and without declarations every call through this module widens to `any` — which
6
+ * silently removes the type checking from the one place in the codebase that decides whether a
7
+ * customer is over quota. Declaring it here rather than in the consumer keeps one definition:
8
+ * a copy in each TypeScript service would be a second description of this contract, free to drift
9
+ * from the implementation without anything failing.
10
+ */
11
+
12
+ export interface EntitlementResult {
13
+ /** False only on a deliberate refusal — never on an infrastructure failure. */
14
+ allowed: boolean;
15
+ unlocked: boolean;
16
+ /** True when this feature is counted; consume() is a no-op otherwise. */
17
+ metered: boolean;
18
+ quota: number | null;
19
+ used: number;
20
+ /** Plan allowance left PLUS unspent à-la-carte credits. */
21
+ remaining: number | null;
22
+ /** Unspent à-la-carte units bought for this subject and feature. */
23
+ credits?: number;
24
+ /** True when the plan allowance is gone and the next call spends a purchased credit. */
25
+ usingCredit?: boolean;
26
+ overage_policy: string;
27
+ reason?: string;
28
+ period?: string | null;
29
+ subject_key?: string | null;
30
+ /** Carried so consume() counts against the exact bucket that was just checked. */
31
+ carriedSubscriptionId?: unknown;
32
+ carriedSubjectType?: string;
33
+ carriedSubjectId?: string;
34
+ carriedPeriodStart?: Date | string | null;
35
+ carriedIdentity?: { accountId?: string | null; userId?: string | null };
36
+ carriedRow?: Record<string, unknown> | null;
37
+ }
38
+
39
+ export interface ConsumeResult {
40
+ consumed: boolean;
41
+ /** The unit came out of a purchased credit rather than the plan's period counter. */
42
+ fromCredit?: boolean;
43
+ /** "not_metered" | "quota_exceeded" | "error" */
44
+ reason?: string;
45
+ }
46
+
47
+ export interface EntitlementStore {
48
+ findSubscription(args: {
49
+ subjectType: string;
50
+ subjectId: string;
51
+ }): Promise<Record<string, unknown> | null>;
52
+
53
+ entitlementFor(args: {
54
+ subjectType: string;
55
+ subjectId: string;
56
+ featureCode: string;
57
+ feature?: Record<string, unknown>;
58
+ identity?: { accountId?: string | null; userId?: string | null };
59
+ bypass?: boolean;
60
+ now?: Date;
61
+ }): Promise<EntitlementResult>;
62
+
63
+ consume(args: {
64
+ entitlement?: EntitlementResult;
65
+ featureCode: string;
66
+ n?: number;
67
+ now?: Date;
68
+ subscriptionId?: unknown;
69
+ row?: Record<string, unknown> | null;
70
+ identity?: { accountId?: string | null; userId?: string | null };
71
+ periodStart?: Date | string | null;
72
+ }): Promise<ConsumeResult>;
73
+
74
+ /** Idempotent; returns null when there is no default plan or the write failed. */
75
+ provisionDefault(args: {
76
+ subjectType: string;
77
+ subjectId: string;
78
+ accountId?: string | null;
79
+ now?: Date;
80
+ }): Promise<Record<string, unknown> | null>;
81
+ }
82
+
83
+ export function createEntitlementStore(opts: {
84
+ /**
85
+ * The subscription master switch. Returning false makes the whole store inert — every
86
+ * entitlement answers OPEN, nothing is counted and no subject is auto-provisioned — so the code
87
+ * can ship to production long before the product does. Defaults to the `SUBSCRIPTION_ENABLED`
88
+ * environment variable, and therefore to OFF.
89
+ */
90
+ isEnabled?: () => boolean | Promise<boolean>;
91
+ /** Returns a raw MongoDB `Db`, or null/undefined when the connection is not ready. */
92
+ getDb: () => unknown;
93
+ /** Cast an id the way the caller's driver expects. Defaults to pass-through. */
94
+ toObjectId?: (v: string) => unknown;
95
+ logger?: { warn: (msg: string) => void };
96
+ }): EntitlementStore;
97
+
98
+ declare const _default: { createEntitlementStore: typeof createEntitlementStore };
99
+ export default _default;