@saasicat/nest 1.0.0-rc.2 → 1.0.0-rc.21

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.
Files changed (154) hide show
  1. package/README.md +44 -8
  2. package/dist/.build-stamp +1 -1
  3. package/dist/_entries.cjs +10214 -4796
  4. package/dist/admin/index.d.cts +5 -4
  5. package/dist/admin/index.d.ts +5 -4
  6. package/dist/admin/index.js +13 -9
  7. package/dist/{admin-resources.module-CpvzcfCc.d.cts → admin-resources.module-B4XNvdH-.d.cts} +1 -2
  8. package/dist/{admin-resources.module-DF5_Q7RD.d.ts → admin-resources.module-CWqdI9BV.d.ts} +1 -2
  9. package/dist/{admin-stats.service-TYiSUwIR.d.ts → admin-stats.service-CsLNt19Z.d.ts} +11 -7
  10. package/dist/{admin-stats.service-DfKupywR.d.cts → admin-stats.service-DrZrU8IJ.d.cts} +11 -7
  11. package/dist/{aggregation-B3CD0v_d.d.cts → aggregation-ChpZDh0g.d.ts} +39 -6
  12. package/dist/{aggregation-BFzW07DE.d.ts → aggregation-OpnhwzF5.d.cts} +39 -6
  13. package/dist/billing/index.d.cts +448 -108
  14. package/dist/billing/index.d.ts +448 -108
  15. package/dist/billing/index.js +108 -41
  16. package/dist/catalog/index.d.cts +74 -45
  17. package/dist/catalog/index.d.ts +74 -45
  18. package/dist/catalog/index.js +21 -13
  19. package/dist/catalog.module-C9O7BxRq.d.ts +180 -0
  20. package/dist/catalog.module-T-I5Ricz.d.cts +180 -0
  21. package/dist/checkout-offer/index.d.cts +17 -22
  22. package/dist/checkout-offer/index.d.ts +17 -22
  23. package/dist/checkout-offer/index.js +21 -7
  24. package/dist/checkout-offer.module-DFhGy6JO.d.cts +58 -0
  25. package/dist/checkout-offer.module-x7SP_dX4.d.ts +58 -0
  26. package/dist/checkout-offer.service-BeHgYikI.d.ts +242 -0
  27. package/dist/checkout-offer.service-CCVxG9b2.d.cts +242 -0
  28. package/dist/chunk-34XVPR6P.js +222 -0
  29. package/dist/{chunk-AOQJEYOL.js → chunk-3QVGA6JX.js} +3 -1
  30. package/dist/chunk-3RLWML2S.js +896 -0
  31. package/dist/{chunk-TXC3LHHB.js → chunk-4SDSYV4A.js} +21 -13
  32. package/dist/chunk-55QEDAMX.js +946 -0
  33. package/dist/chunk-5CKC7O52.js +140 -0
  34. package/dist/chunk-6PJE7EWO.js +185 -0
  35. package/dist/chunk-7NJKVC5X.js +353 -0
  36. package/dist/{chunk-RUNZ3X4C.js → chunk-AOS2JPDD.js} +1263 -185
  37. package/dist/{chunk-WOVQPXV4.js → chunk-CQ2ZZMTD.js} +702 -725
  38. package/dist/{chunk-NA6O63B7.js → chunk-F5X66HF5.js} +5 -5
  39. package/dist/chunk-FEQZCWUL.js +174 -0
  40. package/dist/{chunk-VIJ5NJWC.js → chunk-G6EZWECL.js} +107 -31
  41. package/dist/chunk-G7RQO2XO.js +0 -0
  42. package/dist/{chunk-N3L3AICU.js → chunk-GPQWGA6B.js} +5 -6
  43. package/dist/chunk-GZTL64QK.js +7 -0
  44. package/dist/{chunk-SABTXESR.js → chunk-J7NJRK3K.js} +17 -1
  45. package/dist/{chunk-SEPN52AD.js → chunk-JBCC6C3M.js} +566 -307
  46. package/dist/{chunk-I7GTA3RX.js → chunk-JQRA724W.js} +1 -1
  47. package/dist/{chunk-WXYJHZCN.js → chunk-JVAEKTJ4.js} +2 -14
  48. package/dist/chunk-KPEMTBOP.js +26 -0
  49. package/dist/chunk-LF6J4YYN.js +301 -0
  50. package/dist/{chunk-LTT736P3.js → chunk-LLYVYRGJ.js} +228 -387
  51. package/dist/{chunk-6Z7JR4EW.js → chunk-M47BNEY2.js} +1897 -633
  52. package/dist/{chunk-KFT5AIIH.js → chunk-NDLC5GYK.js} +206 -24
  53. package/dist/{chunk-O2J2HDXA.js → chunk-OOTEXP47.js} +3 -8
  54. package/dist/chunk-QPVDCKYS.js +702 -0
  55. package/dist/{chunk-NHVDCYK5.js → chunk-R5YCJBHY.js} +71 -106
  56. package/dist/{chunk-XBYAFEOR.js → chunk-S33SO5XX.js} +1 -1
  57. package/dist/chunk-SZ7RFPXA.js +10 -0
  58. package/dist/chunk-TBBZWZQT.js +98 -0
  59. package/dist/chunk-TOEFN7DN.js +295 -0
  60. package/dist/{chunk-7ZEGFL42.js → chunk-WHJSYKNC.js} +6 -6
  61. package/dist/chunk-WOEJ6K7M.js +55 -0
  62. package/dist/{chunk-AU3OOREM.js → chunk-XSSWYPP5.js} +16 -3
  63. package/dist/contract-line-item-money-B0Z3Ogur.d.cts +43 -0
  64. package/dist/contract-line-item-money-B0Z3Ogur.d.ts +43 -0
  65. package/dist/discovery/index.d.cts +2 -2
  66. package/dist/discovery/index.d.ts +2 -2
  67. package/dist/discovery/index.js +6 -6
  68. package/dist/{discovery.scanner-9cMqF95o.d.cts → discovery.scanner-DXKc6JkV.d.cts} +1 -1
  69. package/dist/{discovery.scanner-9cMqF95o.d.ts → discovery.scanner-DXKc6JkV.d.ts} +1 -1
  70. package/dist/{enforce-quota.interceptor-Df0e3OWe.d.cts → enforce-quota.interceptor-BFyVdWe8.d.cts} +6 -6
  71. package/dist/{enforce-quota.interceptor-DW3etVyI.d.ts → enforce-quota.interceptor-GaBXboGx.d.ts} +6 -6
  72. package/dist/entitlement/index.d.cts +27 -4
  73. package/dist/entitlement/index.d.ts +27 -4
  74. package/dist/entitlement/index.js +13 -8
  75. package/dist/index.d.cts +209 -28
  76. package/dist/index.d.ts +209 -28
  77. package/dist/index.js +285 -163
  78. package/dist/issuer-identity.check-CBO0vcsq.d.cts +197 -0
  79. package/dist/issuer-identity.check-Cz9CEMkZ.d.ts +197 -0
  80. package/dist/{module-options-COSEb0VW.d.ts → module-options-DmQ3G4sZ.d.ts} +135 -41
  81. package/dist/{module-options-BG3MzQva.d.cts → module-options-qAOwinHR.d.cts} +135 -41
  82. package/dist/payment-callback.service-CR84Xw1Z.d.cts +125 -0
  83. package/dist/payment-callback.service-CR84Xw1Z.d.ts +125 -0
  84. package/dist/payments/index.cjs +4 -0
  85. package/dist/payments/index.d.cts +187 -0
  86. package/dist/payments/index.d.ts +187 -0
  87. package/dist/payments/index.js +141 -0
  88. package/dist/payments.module-BdUq9ot_.d.cts +57 -0
  89. package/dist/payments.module-CQGv_VG7.d.ts +57 -0
  90. package/dist/plan-catalog-source-DWe-BGY1.d.cts +21 -0
  91. package/dist/plan-catalog-source-DWe-BGY1.d.ts +21 -0
  92. package/dist/{plan-resolution-Cgo_TR1H.d.cts → plan-resolution-CwUy_OqC.d.cts} +14 -0
  93. package/dist/{plan-resolution-Cgo_TR1H.d.ts → plan-resolution-CwUy_OqC.d.ts} +14 -0
  94. package/dist/{plan-versions.service-DChhbK6h.d.ts → plan-versions.service-C0N-Em2W.d.ts} +38 -64
  95. package/dist/{plan-versions.service-TPEtbN0r.d.cts → plan-versions.service-DARLh-qI.d.cts} +38 -64
  96. package/dist/platform/index.d.cts +52 -23
  97. package/dist/platform/index.d.ts +52 -23
  98. package/dist/platform/index.js +57 -36
  99. package/dist/promo/index.d.cts +23 -8
  100. package/dist/promo/index.d.ts +23 -8
  101. package/dist/promo/index.js +15 -7
  102. package/dist/{promo.module-DtvIycpt.d.cts → promo.module-CKkECmU8.d.cts} +9 -3
  103. package/dist/{promo.module-Z8h1PNX-.d.ts → promo.module-DuIJdK3U.d.ts} +9 -3
  104. package/dist/promo.service-BYBu6dy3.d.cts +215 -0
  105. package/dist/promo.service-CFjfF0Xm.d.ts +215 -0
  106. package/dist/registration/index.d.cts +104 -47
  107. package/dist/registration/index.d.ts +104 -47
  108. package/dist/registration/index.js +15 -9
  109. package/dist/subscriber/index.cjs +4 -0
  110. package/dist/subscriber/index.d.cts +28 -0
  111. package/dist/subscriber/index.d.ts +28 -0
  112. package/dist/subscriber/index.js +16 -0
  113. package/dist/subscriber.service-5W6Gi-1B.d.cts +68 -0
  114. package/dist/subscriber.service-5W6Gi-1B.d.ts +68 -0
  115. package/dist/subscription-contract/index.d.cts +4 -2
  116. package/dist/subscription-contract/index.d.ts +4 -2
  117. package/dist/subscription-contract/index.js +13 -5
  118. package/dist/{subscription-contract.module-fDFdQ0jm.d.ts → subscription-contract.module-BfIRpc1o.d.ts} +8 -2
  119. package/dist/{subscription-contract.module-CEK8HS5-.d.cts → subscription-contract.module-DTMuUJYd.d.cts} +8 -2
  120. package/dist/{subscription-contract.service-hD87MKyg.d.cts → subscription-contract.service-CPZrTM9C.d.ts} +32 -3
  121. package/dist/{subscription-contract.service-hD87MKyg.d.ts → subscription-contract.service-CfNApLuP.d.cts} +32 -3
  122. package/dist/tenant-billing.controller-BLqbsONe.d.ts +812 -0
  123. package/dist/tenant-billing.controller-DKbAqTbM.d.cts +812 -0
  124. package/dist/tenant-billing.module-ClbjzocC.d.cts +364 -0
  125. package/dist/tenant-billing.module-DohgJJ2m.d.ts +364 -0
  126. package/dist/tenant-billing.tokens-G-1xOlMr.d.cts +129 -0
  127. package/dist/tenant-billing.tokens-G-1xOlMr.d.ts +129 -0
  128. package/dist/testing/index.d.cts +44 -17
  129. package/dist/testing/index.d.ts +44 -17
  130. package/dist/testing/index.js +189 -40
  131. package/package.json +24 -4
  132. package/dist/catalog.module-BDDO6iwq.d.cts +0 -103
  133. package/dist/catalog.module-BTnMsE6u.d.ts +0 -103
  134. package/dist/checkout-offer.module-CMtiQJTT.d.ts +0 -39
  135. package/dist/checkout-offer.module-CVpWrzbe.d.cts +0 -39
  136. package/dist/checkout-offer.service-BLOv2HOo.d.cts +0 -49
  137. package/dist/checkout-offer.service-BLOv2HOo.d.ts +0 -49
  138. package/dist/chunk-57V6ZTI6.js +0 -22
  139. package/dist/chunk-BSK6YBLI.js +0 -87
  140. package/dist/chunk-DQKCK7DX.js +0 -217
  141. package/dist/chunk-VXEYLNIB.js +0 -632
  142. package/dist/chunk-WUDYIPYH.js +0 -715
  143. package/dist/define-saasicat-DROBe-b1.d.ts +0 -69
  144. package/dist/define-saasicat-vGBCkFys.d.cts +0 -69
  145. package/dist/promo.service-BJbKAmw3.d.cts +0 -112
  146. package/dist/promo.service-BJbKAmw3.d.ts +0 -112
  147. package/dist/tenant-billing.controller-BQ67mlmI.d.ts +0 -438
  148. package/dist/tenant-billing.controller-DS8kB8a7.d.cts +0 -438
  149. package/dist/tenant-billing.module-BznC3XtW.d.cts +0 -299
  150. package/dist/tenant-billing.module-wUbsQSFG.d.ts +0 -299
  151. /package/dist/{chunk-2FR6ZL7R.js → chunk-BHZOH2DY.js} +0 -0
  152. /package/dist/{chunk-2SLTRKXC.js → chunk-DFOW3JVO.js} +0 -0
  153. /package/dist/{chunk-DBZGV3HC.js → chunk-DU56UKEH.js} +0 -0
  154. /package/dist/{chunk-OYNHOY45.js → chunk-GAUEVIAE.js} +0 -0
@@ -1,12 +1,12 @@
1
- import {
2
- DiscoverySnapshotNotFoundError,
3
- loadDiscoverySnapshotFromFile
4
- } from "./chunk-74SCJ4HF.js";
5
1
  import {
6
2
  ADVISORY_STRICT_MODE_CODES,
7
3
  validateBundleDraft,
8
4
  validatePlanDraft
9
- } from "./chunk-WOVQPXV4.js";
5
+ } from "./chunk-CQ2ZZMTD.js";
6
+ import {
7
+ DiscoverySnapshotNotFoundError,
8
+ loadDiscoverySnapshotFromFile
9
+ } from "./chunk-74SCJ4HF.js";
10
10
  import {
11
11
  __name
12
12
  } from "./chunk-SHUYVCID.js";
@@ -0,0 +1,174 @@
1
+ import {
2
+ asProvider
3
+ } from "./chunk-J7NJRK3K.js";
4
+ import {
5
+ __name
6
+ } from "./chunk-SHUYVCID.js";
7
+
8
+ // src/billing/plan-catalog-from-snapshot.ts
9
+ function buildPlanCatalogFromSnapshot(settings, snapshot) {
10
+ const liveByPlanKey = new Map(snapshot.livePlanVersions.map((v) => [
11
+ v.planId,
12
+ v
13
+ ]));
14
+ const plans = snapshot.plans.filter((p) => p.deletedAt === null).sort((a, b) => a.sortOrder - b.sortOrder || byKey(a.planKey, b.planKey)).map((stem) => {
15
+ const live = liveByPlanKey.get(stem.planKey);
16
+ if (!live) {
17
+ return {
18
+ id: stem.planKey,
19
+ name: stem.label,
20
+ tagline: stem.description ?? void 0,
21
+ marketed: false,
22
+ monthlyNet: null,
23
+ yearlyNet: null,
24
+ features: [],
25
+ quotas: {}
26
+ };
27
+ }
28
+ return planDefFromVersion({
29
+ id: stem.planKey,
30
+ name: stem.label,
31
+ tagline: stem.description ?? void 0
32
+ }, live);
33
+ });
34
+ const features = snapshot.featureEntries.filter((f) => f.deletedAt === null).sort((a, b) => a.sortOrder - b.sortOrder || byKey(a.featureKey, b.featureKey)).map((row) => ({
35
+ key: row.featureKey,
36
+ label: row.label,
37
+ icon: row.icon ?? void 0,
38
+ tier: row.tier ?? void 0,
39
+ plannedOnly: row.plannedOnly
40
+ }));
41
+ const given = Object.fromEntries(Object.entries(settings).filter(([, value]) => value !== void 0));
42
+ return {
43
+ schemaVersion: 1,
44
+ ...given,
45
+ features,
46
+ plans
47
+ };
48
+ }
49
+ __name(buildPlanCatalogFromSnapshot, "buildPlanCatalogFromSnapshot");
50
+ function planDefFromVersion(identity, version) {
51
+ return {
52
+ ...identity,
53
+ marketed: version.marketed,
54
+ monthlyNet: priceOf(version.monthlyNet),
55
+ yearlyNet: priceOf(version.yearlyNet),
56
+ features: version.features,
57
+ quotas: version.quotas ?? {}
58
+ };
59
+ }
60
+ __name(planDefFromVersion, "planDefFromVersion");
61
+ function priceOf(value) {
62
+ const price = Number.parseFloat(String(value));
63
+ return Number.isFinite(price) ? price : null;
64
+ }
65
+ __name(priceOf, "priceOf");
66
+ function byKey(a, b) {
67
+ if (a === b) return 0;
68
+ return a < b ? -1 : 1;
69
+ }
70
+ __name(byKey, "byKey");
71
+
72
+ // src/billing/plan-catalog-source.ts
73
+ function databasePlanCatalogSource(settings, sink) {
74
+ return {
75
+ origin: "database",
76
+ current: /* @__PURE__ */ __name(async () => buildPlanCatalogFromSnapshot(settings, await sink.loadSnapshot()), "current")
77
+ };
78
+ }
79
+ __name(databasePlanCatalogSource, "databasePlanCatalogSource");
80
+ function givenPlanCatalogSource(catalog) {
81
+ return {
82
+ origin: "given",
83
+ current: /* @__PURE__ */ __name(() => Promise.resolve(catalog), "current")
84
+ };
85
+ }
86
+ __name(givenPlanCatalogSource, "givenPlanCatalogSource");
87
+
88
+ // src/billing/plan-catalog.module.ts
89
+ import { Module } from "@nestjs/common";
90
+ import { planCatalogSettingsOf } from "@saasicat/core";
91
+ function _ts_decorate(decorators, target, key, desc) {
92
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
93
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
94
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
95
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
96
+ }
97
+ __name(_ts_decorate, "_ts_decorate");
98
+ var PLAN_CATALOG_SETTINGS_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/nest/PlanCatalogSettings");
99
+ var PLAN_CATALOG_SOURCE_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/nest/PlanCatalogSource");
100
+ var PLAN_CATALOG_READ_SINK_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/nest/PLAN_CATALOG_READ_SINK");
101
+ var PlanCatalogModule = class _PlanCatalogModule {
102
+ static {
103
+ __name(this, "PlanCatalogModule");
104
+ }
105
+ static forRoot(options) {
106
+ const { sink: sinkSpec, imports, extraProviders, global, ...settings } = options;
107
+ return {
108
+ module: _PlanCatalogModule,
109
+ global: global ?? true,
110
+ imports: imports ?? [],
111
+ providers: [
112
+ ...extraProviders ?? [],
113
+ asProvider(PLAN_CATALOG_READ_SINK_TOKEN, sinkSpec),
114
+ {
115
+ provide: PLAN_CATALOG_SETTINGS_TOKEN,
116
+ useValue: settings
117
+ },
118
+ {
119
+ provide: PLAN_CATALOG_SOURCE_TOKEN,
120
+ useFactory: /* @__PURE__ */ __name(async (sink) => {
121
+ const source = databasePlanCatalogSource(settings, sink);
122
+ await source.current();
123
+ return source;
124
+ }, "useFactory"),
125
+ inject: [
126
+ PLAN_CATALOG_READ_SINK_TOKEN
127
+ ]
128
+ }
129
+ ],
130
+ exports: [
131
+ PLAN_CATALOG_SETTINGS_TOKEN,
132
+ PLAN_CATALOG_SOURCE_TOKEN,
133
+ PLAN_CATALOG_READ_SINK_TOKEN
134
+ ]
135
+ };
136
+ }
137
+ /**
138
+ * Tests / in-memory setup: takes the catalog object directly, without a
139
+ * sink/DB lookup. For unit tests + test bootstraps.
140
+ */
141
+ static forRootWithCatalog(catalog, opts = {}) {
142
+ return {
143
+ module: _PlanCatalogModule,
144
+ providers: [
145
+ {
146
+ provide: PLAN_CATALOG_SETTINGS_TOKEN,
147
+ useValue: planCatalogSettingsOf(catalog)
148
+ },
149
+ {
150
+ provide: PLAN_CATALOG_SOURCE_TOKEN,
151
+ useValue: givenPlanCatalogSource(catalog)
152
+ }
153
+ ],
154
+ exports: [
155
+ PLAN_CATALOG_SETTINGS_TOKEN,
156
+ PLAN_CATALOG_SOURCE_TOKEN
157
+ ],
158
+ global: opts.global ?? true
159
+ };
160
+ }
161
+ };
162
+ PlanCatalogModule = _ts_decorate([
163
+ Module({})
164
+ ], PlanCatalogModule);
165
+
166
+ export {
167
+ buildPlanCatalogFromSnapshot,
168
+ planDefFromVersion,
169
+ givenPlanCatalogSource,
170
+ PLAN_CATALOG_SETTINGS_TOKEN,
171
+ PLAN_CATALOG_SOURCE_TOKEN,
172
+ PLAN_CATALOG_READ_SINK_TOKEN,
173
+ PlanCatalogModule
174
+ };
@@ -1,25 +1,27 @@
1
- import {
2
- PLAN_CATALOG_TOKEN,
3
- isFeaturePlannedOnly
4
- } from "./chunk-DQKCK7DX.js";
5
- import {
6
- SUBSCRIPTION_CONTRACT_REPOSITORY_TOKEN
7
- } from "./chunk-VBRYPU7V.js";
8
- import {
9
- codedError
10
- } from "./chunk-57V6ZTI6.js";
11
1
  import {
12
2
  ENTITLEMENT_RESOLUTION_CONFIG_TOKEN,
13
3
  PLAN_VERSION_REPOSITORY_TOKEN,
14
4
  SUBSCRIPTION_REPOSITORY_TOKEN,
15
5
  TRANSACTION_RUNNER_TOKEN
16
6
  } from "./chunk-OUDSQ3PU.js";
7
+ import {
8
+ isFeaturePlannedOnly
9
+ } from "./chunk-TBBZWZQT.js";
10
+ import {
11
+ SUBSCRIPTION_CONTRACT_REPOSITORY_TOKEN
12
+ } from "./chunk-VBRYPU7V.js";
17
13
  import {
18
14
  BUNDLE_REPOSITORY_TOKEN
19
- } from "./chunk-AOQJEYOL.js";
15
+ } from "./chunk-3QVGA6JX.js";
20
16
  import {
21
17
  DISCOVERY_SNAPSHOT_TOKEN
22
18
  } from "./chunk-QUZU2M33.js";
19
+ import {
20
+ PLAN_CATALOG_SOURCE_TOKEN
21
+ } from "./chunk-FEQZCWUL.js";
22
+ import {
23
+ codedError
24
+ } from "./chunk-J7NJRK3K.js";
23
25
  import {
24
26
  __name
25
27
  } from "./chunk-SHUYVCID.js";
@@ -28,6 +30,17 @@ import {
28
30
  var SUBSCRIPTION_BUNDLE_REPOSITORY_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/nest/SubscriptionBundleRepository");
29
31
  var SUBSCRIPTION_BUNDLE_CONFIG_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/nest/SubscriptionBundleConfig");
30
32
 
33
+ // src/entitlement/landed-cancellation.ts
34
+ function cancellationHasLanded(sub, now) {
35
+ const landsAt = cancellationLandsAt(sub);
36
+ return landsAt !== null && landsAt <= now;
37
+ }
38
+ __name(cancellationHasLanded, "cancellationHasLanded");
39
+ function cancellationLandsAt(sub) {
40
+ return sub.canceledEffectiveAt ?? sub.canceledAt;
41
+ }
42
+ __name(cancellationLandsAt, "cancellationLandsAt");
43
+
31
44
  // src/entitlement/aggregation.ts
32
45
  function filterActiveSubscriptionBundles(bundles, now) {
33
46
  return bundles.filter((b) => b.canceledEffectiveAt === null || b.canceledEffectiveAt > now);
@@ -332,7 +345,7 @@ var EntitlementService = class {
332
345
  static {
333
346
  __name(this, "EntitlementService");
334
347
  }
335
- catalog;
348
+ catalogs;
336
349
  subscriptions;
337
350
  planVersions;
338
351
  tx;
@@ -350,8 +363,8 @@ var EntitlementService = class {
350
363
  // #39 — replaces alias index, built lazily from the boot-static snapshot;
351
364
  // throws on replaces cycles (a clear error instead of an infinite loop).
352
365
  replacedByIndex = null;
353
- constructor(catalog, subscriptions, planVersions, tx, resolutionConfig = null, subscriptionBundles = null, bundles = null, subscriptionContracts = null, discoverySnapshot = null) {
354
- this.catalog = catalog;
366
+ constructor(catalogs, subscriptions, planVersions, tx, resolutionConfig = null, subscriptionBundles = null, bundles = null, subscriptionContracts = null, discoverySnapshot = null) {
367
+ this.catalogs = catalogs;
355
368
  this.subscriptions = subscriptions;
356
369
  this.planVersions = planVersions;
357
370
  this.tx = tx;
@@ -364,9 +377,19 @@ var EntitlementService = class {
364
377
  // ---------------------------------------------------------------------
365
378
  // Read path — for FeatureGuard, sidebar hooks, GET /billing/entitlement
366
379
  // ---------------------------------------------------------------------
367
- async computeLimits(tenantId, now = /* @__PURE__ */ new Date()) {
368
- const cached = this.readCache(tenantId, now.getTime());
369
- if (cached) return cached;
380
+ /**
381
+ * The tenant's effective limits, answered from the cache for up to
382
+ * `CACHE_TTL_MS`. A caller that has already read the catalogue for its own
383
+ * operation passes it as `catalog`: the answer is then computed against
384
+ * that reading and kept out of the cache in both directions — not taken
385
+ * from it, and not written to it, since a reading the caller holds may be
386
+ * older than the one everybody else should be answered from.
387
+ */
388
+ async computeLimits(tenantId, now = /* @__PURE__ */ new Date(), catalog) {
389
+ if (!catalog) {
390
+ const cached = this.readCache(tenantId, now.getTime());
391
+ if (cached) return cached;
392
+ }
370
393
  const sub = await this.subscriptions.findByTenantId(tenantId);
371
394
  if (!sub) {
372
395
  throw new NotFoundException({
@@ -377,8 +400,9 @@ var EntitlementService = class {
377
400
  }
378
401
  });
379
402
  }
380
- const limits = await this.deriveLimits(sub, now);
381
- this.writeCache(tenantId, limits, now.getTime());
403
+ if (catalog) return this.limitsFor(sub, now, catalog);
404
+ const limits = await this.limitsFor(sub, now, await this.catalogs.current());
405
+ this.writeCache(tenantId, limits, now.getTime(), cancellationLandsAt(sub));
382
406
  return limits;
383
407
  }
384
408
  /**
@@ -399,27 +423,75 @@ var EntitlementService = class {
399
423
  * have a `SubscriptionRecord` (e.g. within a transaction).
400
424
  */
401
425
  async deriveLimits(sub, now, tx) {
426
+ return this.limitsFor(sub, now, await this.catalogs.current(), tx);
427
+ }
428
+ /**
429
+ * `deriveLimits` against a catalogue already read. The catalogue decides
430
+ * which features are `plannedOnly`, and it is read outside any transaction
431
+ * a caller holds, so that checking a limit does not wait for a second
432
+ * connection while the subscription row is locked.
433
+ */
434
+ async limitsFor(sub, now, catalog, tx) {
435
+ if (cancellationHasLanded(sub, now)) {
436
+ const floor = this.resolutionConfig?.canceledEntitlementPlan;
437
+ if (floor === void 0) {
438
+ return {
439
+ plan: sub.plan,
440
+ quotas: {},
441
+ features: /* @__PURE__ */ new Set()
442
+ };
443
+ }
444
+ const floorVersion = await this.findActivePlanVersionOrFallback(floor, now, tx);
445
+ return this.asGrantable(catalog, aggregateLimits({
446
+ plan: floor,
447
+ planVersion: floorVersion,
448
+ subscriptionBundles: [],
449
+ customLimits: null
450
+ }, catalog, now));
451
+ }
402
452
  const contract = await this.findActiveContract(sub.tenantId, now, tx);
403
453
  if (contract) {
404
454
  const bundles = await this.loadSubscriptionBundleSnapshots(sub.id, now, tx);
405
- return this.withReplacedFeatureAliases(mergeSubscriptionBundlesIntoLimits(contractLimits(contract), bundles, contractBundleVersionIds(contract), this.catalog, now));
455
+ return this.asGrantable(catalog, mergeSubscriptionBundlesIntoLimits(contractLimits(contract), bundles, contractBundleVersionIds(contract), catalog, now));
406
456
  }
407
457
  const effectivePlan = resolveEntitlementPlan(sub, this.resolutionConfig ?? {}, now);
408
458
  const planVersion = effectivePlan === sub.plan ? sub.planVersion : await this.findActivePlanVersionOrFallback(effectivePlan, now, tx);
409
459
  const subscriptionBundles = await this.loadSubscriptionBundleSnapshots(sub.id, now, tx);
410
- return this.withReplacedFeatureAliases(aggregateLimits({
460
+ return this.asGrantable(catalog, aggregateLimits({
411
461
  plan: effectivePlan,
412
462
  planVersion,
413
463
  subscriptionBundles,
414
464
  customLimits: sub.customLimits ?? null
415
- }, this.catalog, now));
465
+ }, catalog, now));
466
+ }
467
+ /**
468
+ * The last thing every path does, so that no path can skip it.
469
+ *
470
+ * Two steps, in this order. Grandfathering (#39): granted old feature keys
471
+ * transitively grant their successors from the `replaces` chains of the
472
+ * discovery snapshot — a no-op without a snapshot or without replaces
473
+ * declarations. Then `plannedOnly` features come out.
474
+ *
475
+ * The filter sits here rather than in each branch because it used to sit
476
+ * in each branch and one of them did not have it: limits read from a
477
+ * frozen contract were handed over as they stood, so a feature the
478
+ * catalogue says has no code behind it was granted to whoever had signed
479
+ * before it was marked. A successor pulled in by an alias could arrive the
480
+ * same way. `SC-ENTL-003` says "never … wherever it comes from", and one
481
+ * place is what makes that answerable.
482
+ */
483
+ asGrantable(catalog, limits) {
484
+ return {
485
+ ...limits,
486
+ features: filterPlannedOnlyFeatures(this.replaceFeatureAliases(limits).features, catalog)
487
+ };
416
488
  }
417
489
  /**
418
490
  * Grandfathering (#39): granted old feature keys transitively grant
419
491
  * their successors from the `replaces` chains of the discovery snapshot.
420
492
  * A no-op without a snapshot or without replaces declarations.
421
493
  */
422
- withReplacedFeatureAliases(limits) {
494
+ replaceFeatureAliases(limits) {
423
495
  if (!this.discoverySnapshot) return limits;
424
496
  if (!this.replacedByIndex) {
425
497
  this.replacedByIndex = buildReplacedByIndex(this.discoverySnapshot.features);
@@ -467,6 +539,7 @@ var EntitlementService = class {
467
539
  async enforceLimit(input) {
468
540
  const now = input.now ?? /* @__PURE__ */ new Date();
469
541
  const delta = input.delta ?? 1;
542
+ const catalog = await this.catalogs.current();
470
543
  return this.tx.run(async (tx) => {
471
544
  const sub = await this.subscriptions.findByTenantIdLocked(input.tenantId, tx);
472
545
  if (!sub) {
@@ -478,12 +551,11 @@ var EntitlementService = class {
478
551
  }
479
552
  });
480
553
  }
481
- const limits = await this.deriveLimits(sub, now, tx);
554
+ const limits = await this.limitsFor(sub, now, catalog, tx);
482
555
  const max = limits.quotas[input.dimension];
483
556
  if (max === void 0) {
484
557
  throw new InternalServerErrorException(codedError(BILLING_ERROR_CODES.QUOTA_DIMENSION_UNKNOWN, {
485
- dimension: input.dimension,
486
- projectKey: this.catalog.projectKey
558
+ dimension: input.dimension
487
559
  }));
488
560
  }
489
561
  const used = await input.currentUsage(tx, input.tenantId);
@@ -520,7 +592,7 @@ var EntitlementService = class {
520
592
  readCache(tenantId, nowMs) {
521
593
  const entry = this.cache.get(tenantId);
522
594
  if (!entry) return null;
523
- if (entry.expiresAt < nowMs) {
595
+ if (entry.expiresAt <= nowMs) {
524
596
  this.cache.delete(tenantId);
525
597
  return null;
526
598
  }
@@ -528,11 +600,13 @@ var EntitlementService = class {
528
600
  this.cache.set(tenantId, entry);
529
601
  return entry.value;
530
602
  }
531
- writeCache(tenantId, value, nowMs) {
603
+ writeCache(tenantId, value, nowMs, expiresNotAfter = null) {
532
604
  if (this.cache.has(tenantId)) this.cache.delete(tenantId);
605
+ const ttlExpiry = nowMs + CACHE_TTL_MS;
606
+ const boundary = expiresNotAfter?.getTime() ?? Infinity;
533
607
  this.cache.set(tenantId, {
534
608
  value,
535
- expiresAt: nowMs + CACHE_TTL_MS
609
+ expiresAt: boundary > nowMs ? Math.min(ttlExpiry, boundary) : ttlExpiry
536
610
  });
537
611
  while (this.cache.size > CACHE_MAX_ENTRIES) {
538
612
  const oldest = this.cache.keys().next().value;
@@ -543,7 +617,7 @@ var EntitlementService = class {
543
617
  };
544
618
  EntitlementService = _ts_decorate([
545
619
  Injectable(),
546
- _ts_param(0, Inject(PLAN_CATALOG_TOKEN)),
620
+ _ts_param(0, Inject(PLAN_CATALOG_SOURCE_TOKEN)),
547
621
  _ts_param(1, Inject(SUBSCRIPTION_REPOSITORY_TOKEN)),
548
622
  _ts_param(2, Inject(PLAN_VERSION_REPOSITORY_TOKEN)),
549
623
  _ts_param(3, Inject(TRANSACTION_RUNNER_TOKEN)),
@@ -559,7 +633,7 @@ EntitlementService = _ts_decorate([
559
633
  _ts_param(8, Inject(DISCOVERY_SNAPSHOT_TOKEN)),
560
634
  _ts_metadata("design:type", Function),
561
635
  _ts_metadata("design:paramtypes", [
562
- typeof PlanCatalog === "undefined" ? Object : PlanCatalog,
636
+ typeof PlanCatalogSource === "undefined" ? Object : PlanCatalogSource,
563
637
  typeof SubscriptionRepository === "undefined" ? Object : SubscriptionRepository,
564
638
  typeof PlanVersionRepository === "undefined" ? Object : PlanVersionRepository,
565
639
  typeof TransactionRunner === "undefined" ? Object : TransactionRunner,
@@ -574,6 +648,8 @@ EntitlementService = _ts_decorate([
574
648
  export {
575
649
  SUBSCRIPTION_BUNDLE_REPOSITORY_TOKEN,
576
650
  SUBSCRIPTION_BUNDLE_CONFIG_TOKEN,
651
+ cancellationHasLanded,
652
+ cancellationLandsAt,
577
653
  filterActiveSubscriptionBundles,
578
654
  aggregateSubscriptionBundleQuotas,
579
655
  collectSubscriptionBundleFeatures,
File without changes
@@ -1,8 +1,6 @@
1
- // src/billing/version-diff.ts
2
- import { classifyBundleVersionDiff, classifyPlanDiff } from "@saasicat/core";
3
-
4
1
  // src/billing/tenant-billing.tokens.ts
5
2
  var TENANT_AUTH_GUARDS_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/nest/TenantAuthGuards");
3
+ var BILLING_PERMISSION_GUARDS_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/nest/BillingPermissionGuards");
6
4
  var TENANT_ID_RESOLVER_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/nest/TenantIdResolver");
7
5
  var USER_ID_RESOLVER_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/nest/UserIdResolver");
8
6
  var USER_EMAIL_RESOLVER_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/nest/UserEmailResolver");
@@ -12,11 +10,11 @@ var USAGE_SNAPSHOT_PORT_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/nest/UsageS
12
10
  var SUBSCRIPTION_WRITE_PORT_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/nest/TenantSubscriptionWritePort");
13
11
  var TRIAL_PROJECTION_PORT_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/nest/TrialProjectionPort");
14
12
  var PENDING_PLAN_QUERY_PORT_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/nest/PendingPlanQueryPort");
13
+ var CANCELLATION_NOTICE_DAYS_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/nest/CancellationNoticeDays");
15
14
 
16
15
  export {
17
- classifyBundleVersionDiff,
18
- classifyPlanDiff,
19
16
  TENANT_AUTH_GUARDS_TOKEN,
17
+ BILLING_PERMISSION_GUARDS_TOKEN,
20
18
  TENANT_ID_RESOLVER_TOKEN,
21
19
  USER_ID_RESOLVER_TOKEN,
22
20
  USER_EMAIL_RESOLVER_TOKEN,
@@ -25,5 +23,6 @@ export {
25
23
  USAGE_SNAPSHOT_PORT_TOKEN,
26
24
  SUBSCRIPTION_WRITE_PORT_TOKEN,
27
25
  TRIAL_PROJECTION_PORT_TOKEN,
28
- PENDING_PLAN_QUERY_PORT_TOKEN
26
+ PENDING_PLAN_QUERY_PORT_TOKEN,
27
+ CANCELLATION_NOTICE_DAYS_TOKEN
29
28
  };
@@ -0,0 +1,7 @@
1
+ // src/billing/version-diff.ts
2
+ import { classifyBundleVersionDiff, classifyPlanDiff } from "@saasicat/core";
3
+
4
+ export {
5
+ classifyBundleVersionDiff,
6
+ classifyPlanDiff
7
+ };
@@ -19,6 +19,22 @@ function asProvider(token, impl) {
19
19
  }
20
20
  __name(asProvider, "asProvider");
21
21
 
22
+ // src/errors/coded-error.ts
23
+ import { ERROR_MESSAGES_EN, formatErrorMessage } from "@saasicat/core";
24
+ function codedError(code, params) {
25
+ const message = formatErrorMessage(ERROR_MESSAGES_EN[code], params ?? {});
26
+ return params ? {
27
+ code,
28
+ message,
29
+ params
30
+ } : {
31
+ code,
32
+ message
33
+ };
34
+ }
35
+ __name(codedError, "codedError");
36
+
22
37
  export {
23
- asProvider
38
+ asProvider,
39
+ codedError
24
40
  };