@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,16 +1,20 @@
1
1
  import {
2
- AUDIT_CONTEXT_RESOLVER_TOKEN,
3
- USER_EMAIL_RESOLVER_TOKEN,
4
- USER_ID_RESOLVER_TOKEN,
5
2
  classifyBundleVersionDiff,
6
3
  classifyPlanDiff
7
- } from "./chunk-N3L3AICU.js";
4
+ } from "./chunk-GZTL64QK.js";
5
+ import {
6
+ EnforceMfa
7
+ } from "./chunk-34XVPR6P.js";
8
8
  import {
9
9
  AdminAuditService
10
- } from "./chunk-O2J2HDXA.js";
10
+ } from "./chunk-OOTEXP47.js";
11
11
  import {
12
- SaaSiCatPublicRoute
13
- } from "./chunk-YSC46BGM.js";
12
+ cyclesSoldFor,
13
+ resolveBundlePriceNet
14
+ } from "./chunk-KPEMTBOP.js";
15
+ import {
16
+ DiscoveryScanner
17
+ } from "./chunk-JVAEKTJ4.js";
14
18
  import {
15
19
  SUBSCRIPTION_REPOSITORY_TOKEN
16
20
  } from "./chunk-OUDSQ3PU.js";
@@ -21,32 +25,129 @@ import {
21
25
  CATALOG_SERVICE_CONFIG_TOKEN,
22
26
  MARKETING_PROJECTION_REPOSITORY_TOKEN,
23
27
  MARKETING_SETTINGS_REPOSITORY_TOKEN,
28
+ NEW_PAYMENT_METHODS_SOURCE_TOKEN,
24
29
  PLAN_REPOSITORY_TOKEN,
25
30
  PROMOTION_REPOSITORY_TOKEN
26
- } from "./chunk-AOQJEYOL.js";
27
- import {
28
- asProvider
29
- } from "./chunk-SABTXESR.js";
30
- import {
31
- DiscoveryScanner
32
- } from "./chunk-WXYJHZCN.js";
31
+ } from "./chunk-3QVGA6JX.js";
33
32
  import {
34
33
  DISCOVERY_SNAPSHOT_TOKEN
35
34
  } from "./chunk-QUZU2M33.js";
35
+ import {
36
+ AUDIT_CONTEXT_RESOLVER_TOKEN,
37
+ USER_EMAIL_RESOLVER_TOKEN,
38
+ USER_ID_RESOLVER_TOKEN
39
+ } from "./chunk-GPQWGA6B.js";
40
+ import {
41
+ SaaSiCatPublicRoute
42
+ } from "./chunk-YSC46BGM.js";
43
+ import {
44
+ asProvider
45
+ } from "./chunk-J7NJRK3K.js";
36
46
  import {
37
47
  __name
38
48
  } from "./chunk-SHUYVCID.js";
39
49
 
50
+ // src/catalog/validity-window.ts
51
+ import { UnprocessableEntityException } from "@nestjs/common";
52
+ import { CATALOG_ERROR_CODES } from "@saasicat/core";
53
+ var DAY_MS = 24 * 60 * 60 * 1e3;
54
+ var PLAN_VERSION_WINDOW_CODES = {
55
+ validFromRequired: CATALOG_ERROR_CODES.PLAN_VERSION_VALID_FROM_REQUIRED,
56
+ validFromInvalid: CATALOG_ERROR_CODES.PLAN_VERSION_VALID_FROM_INVALID,
57
+ validFromNotAfterPrevious: CATALOG_ERROR_CODES.PLAN_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS,
58
+ validFromNotGapless: CATALOG_ERROR_CODES.PLAN_VERSION_VALID_FROM_NOT_GAPLESS,
59
+ validUntilInvalid: CATALOG_ERROR_CODES.PLAN_VERSION_VALID_UNTIL_INVALID,
60
+ validUntilBeforeFrom: CATALOG_ERROR_CODES.PLAN_VERSION_VALID_UNTIL_BEFORE_FROM
61
+ };
62
+ var BUNDLE_VERSION_WINDOW_CODES = {
63
+ validFromRequired: CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_REQUIRED,
64
+ validFromInvalid: CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_INVALID,
65
+ validFromNotAfterPrevious: CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS,
66
+ validFromNotGapless: CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_NOT_GAPLESS,
67
+ validUntilInvalid: CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_UNTIL_INVALID,
68
+ validUntilBeforeFrom: CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_UNTIL_BEFORE_FROM
69
+ };
70
+ function resolveValidityWindow(publishMeta, draft, previous, codes) {
71
+ const validFromInput = publishMeta.validFrom ?? draft.validFrom;
72
+ if (!validFromInput) {
73
+ throw new UnprocessableEntityException({
74
+ code: codes.validFromRequired,
75
+ message: "validFrom must be set when publishing (on the draft or the publish call).."
76
+ });
77
+ }
78
+ const validFrom = new Date(validFromInput);
79
+ if (Number.isNaN(validFrom.getTime())) {
80
+ throw new UnprocessableEntityException({
81
+ code: codes.validFromInvalid,
82
+ message: `validFrom '${validFromInput}' is not a valid date`,
83
+ params: {
84
+ validFrom: validFromInput
85
+ }
86
+ });
87
+ }
88
+ if (previous?.validFrom) {
89
+ const previousFrom = new Date(previous.validFrom);
90
+ if (validFrom <= previousFrom) {
91
+ throw new UnprocessableEntityException({
92
+ code: codes.validFromNotAfterPrevious,
93
+ message: `validFrom (${validFrom.toISOString()}) must be strictly after the validFrom of the previous version (${previous.validFrom}).`,
94
+ params: {
95
+ validFrom: validFrom.toISOString(),
96
+ previousValidFrom: previous.validFrom
97
+ }
98
+ });
99
+ }
100
+ if (previous.validUntil) {
101
+ const requiredStart = new Date(new Date(previous.validUntil).getTime() + DAY_MS);
102
+ if (validFrom.getTime() !== requiredStart.getTime()) {
103
+ throw new UnprocessableEntityException({
104
+ code: codes.validFromNotGapless,
105
+ message: `The predecessor has validUntil=${previous.validUntil.slice(0, 10)} \u2014 the successor must start seamlessly on the next day (${requiredStart.toISOString().slice(0, 10)}). Received: ${validFrom.toISOString().slice(0, 10)}.`,
106
+ params: {
107
+ received: validFrom.toISOString().slice(0, 10)
108
+ },
109
+ requiredValidFrom: requiredStart.toISOString().slice(0, 10),
110
+ previousValidUntil: previous.validUntil
111
+ });
112
+ }
113
+ }
114
+ }
115
+ const validUntilInput = publishMeta.validUntil !== void 0 ? publishMeta.validUntil : draft.validUntil;
116
+ const validUntil = validUntilInput ? new Date(validUntilInput) : null;
117
+ if (validUntil && Number.isNaN(validUntil.getTime())) {
118
+ throw new UnprocessableEntityException({
119
+ code: codes.validUntilInvalid,
120
+ message: `validUntil '${validUntilInput}' is not a valid date`,
121
+ params: {
122
+ validUntil: validUntilInput
123
+ }
124
+ });
125
+ }
126
+ if (validUntil && validUntil <= validFrom) {
127
+ throw new UnprocessableEntityException({
128
+ code: codes.validUntilBeforeFrom,
129
+ message: `validUntil (${validUntil.toISOString()}) must be strictly after validFrom (${validFrom.toISOString()}).`,
130
+ params: {
131
+ validUntil: validUntil.toISOString(),
132
+ validFrom: validFrom.toISOString()
133
+ }
134
+ });
135
+ }
136
+ return {
137
+ validFrom,
138
+ validUntil
139
+ };
140
+ }
141
+ __name(resolveValidityWindow, "resolveValidityWindow");
142
+
40
143
  // src/catalog/approved-keys.ts
41
- async function loadApprovedCatalogKeys(repo, projectKey) {
144
+ async function loadApprovedCatalogKeys(repo) {
42
145
  if (!repo) return null;
43
146
  const [features, quotas] = await Promise.all([
44
147
  repo.listFeatures({
45
- projectKey,
46
148
  discoveryStatus: "approved"
47
149
  }),
48
150
  repo.listQuotas({
49
- projectKey,
50
151
  discoveryStatus: "approved"
51
152
  })
52
153
  ]);
@@ -191,8 +292,8 @@ function validatePlanDraft(draft, snapshot, marketedOnlyFeatures = /* @__PURE__
191
292
  __name(validatePlanDraft, "validatePlanDraft");
192
293
 
193
294
  // src/catalog/bundles.service.ts
194
- import { Inject, Injectable, Logger, NotFoundException, Optional, UnprocessableEntityException } from "@nestjs/common";
195
- import { CATALOG_ERROR_CODES, isVersionEditable } from "@saasicat/core";
295
+ import { Inject, Injectable, Logger, NotFoundException, Optional, UnprocessableEntityException as UnprocessableEntityException2 } from "@nestjs/common";
296
+ import { CATALOG_ERROR_CODES as CATALOG_ERROR_CODES2, isVersionEditable } from "@saasicat/core";
196
297
 
197
298
  // src/core/discovery-snapshot-source.ts
198
299
  function resolveDiscoverySnapshot(injected, scanner) {
@@ -254,9 +355,8 @@ var BundlesService = class _BundlesService {
254
355
  // =========================================================================
255
356
  // Master operations
256
357
  // =========================================================================
257
- listBundles(projectKey) {
358
+ listBundles() {
258
359
  return this.repo.list({
259
- projectKey,
260
360
  excludeDeleted: true
261
361
  });
262
362
  }
@@ -264,7 +364,7 @@ var BundlesService = class _BundlesService {
264
364
  const bundle = await this.repo.findById(bundleId);
265
365
  if (!bundle) {
266
366
  throw new NotFoundException({
267
- code: CATALOG_ERROR_CODES.BUNDLE_NOT_FOUND,
367
+ code: CATALOG_ERROR_CODES2.BUNDLE_NOT_FOUND,
268
368
  message: `Bundle '${bundleId}' not found`,
269
369
  params: {
270
370
  bundleId
@@ -278,14 +378,13 @@ var BundlesService = class _BundlesService {
278
378
  };
279
379
  }
280
380
  async createBundle(data) {
281
- const existing = await this.repo.findByKey(data.projectKey, data.bundleKey);
381
+ const existing = await this.repo.findByKey(data.bundleKey);
282
382
  if (existing) {
283
- throw new UnprocessableEntityException({
284
- code: CATALOG_ERROR_CODES.BUNDLE_ALREADY_EXISTS,
285
- message: `Bundle '${data.bundleKey}' already exists in project '${data.projectKey}'`,
383
+ throw new UnprocessableEntityException2({
384
+ code: CATALOG_ERROR_CODES2.BUNDLE_ALREADY_EXISTS,
385
+ message: `Bundle '${data.bundleKey}' already exists`,
286
386
  params: {
287
- bundleKey: data.bundleKey,
288
- projectKey: data.projectKey
387
+ bundleKey: data.bundleKey
289
388
  }
290
389
  });
291
390
  }
@@ -295,7 +394,7 @@ var BundlesService = class _BundlesService {
295
394
  const existing = await this.repo.findById(bundleId);
296
395
  if (!existing) {
297
396
  throw new NotFoundException({
298
- code: CATALOG_ERROR_CODES.BUNDLE_NOT_FOUND,
397
+ code: CATALOG_ERROR_CODES2.BUNDLE_NOT_FOUND,
299
398
  message: `Bundle '${bundleId}' not found`,
300
399
  params: {
301
400
  bundleId
@@ -308,7 +407,7 @@ var BundlesService = class _BundlesService {
308
407
  const existing = await this.repo.findById(bundleId);
309
408
  if (!existing) {
310
409
  throw new NotFoundException({
311
- code: CATALOG_ERROR_CODES.BUNDLE_NOT_FOUND,
410
+ code: CATALOG_ERROR_CODES2.BUNDLE_NOT_FOUND,
312
411
  message: `Bundle '${bundleId}' not found`,
313
412
  params: {
314
413
  bundleId
@@ -327,7 +426,7 @@ var BundlesService = class _BundlesService {
327
426
  const existing = await this.repo.findVersionById(versionId);
328
427
  if (!existing) {
329
428
  throw new NotFoundException({
330
- code: CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
429
+ code: CATALOG_ERROR_CODES2.BUNDLE_VERSION_NOT_FOUND,
331
430
  message: `BundleVersion '${versionId}' not found`,
332
431
  params: {
333
432
  bundleVersionId: versionId
@@ -335,8 +434,8 @@ var BundlesService = class _BundlesService {
335
434
  });
336
435
  }
337
436
  if (existing.publishedAt !== null) {
338
- throw new UnprocessableEntityException({
339
- code: CATALOG_ERROR_CODES.BUNDLE_VERSION_ALREADY_PUBLISHED,
437
+ throw new UnprocessableEntityException2({
438
+ code: CATALOG_ERROR_CODES2.BUNDLE_VERSION_ALREADY_PUBLISHED,
340
439
  message: `BundleVersion '${versionId}' is already published and cannot be discarded.`,
341
440
  params: {
342
441
  versionId
@@ -344,8 +443,8 @@ var BundlesService = class _BundlesService {
344
443
  });
345
444
  }
346
445
  if (typeof this.repo.deleteDraft !== "function") {
347
- throw new UnprocessableEntityException({
348
- code: CATALOG_ERROR_CODES.BUNDLE_VERSION_DISCARD_NOT_IMPLEMENTED,
446
+ throw new UnprocessableEntityException2({
447
+ code: CATALOG_ERROR_CODES2.BUNDLE_VERSION_DISCARD_NOT_IMPLEMENTED,
349
448
  message: "Discard is not implemented in the current repository. Implement BundleRepository.deleteDraft."
350
449
  });
351
450
  }
@@ -361,7 +460,7 @@ var BundlesService = class _BundlesService {
361
460
  const version = await this.repo.findVersionById(versionId);
362
461
  if (!version) {
363
462
  throw new NotFoundException({
364
- code: CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
463
+ code: CATALOG_ERROR_CODES2.BUNDLE_VERSION_NOT_FOUND,
365
464
  message: `BundleVersion '${versionId}' not found`,
366
465
  params: {
367
466
  bundleVersionId: versionId
@@ -409,7 +508,7 @@ var BundlesService = class _BundlesService {
409
508
  const bundle = await this.repo.findById(data.bundleId);
410
509
  if (!bundle) {
411
510
  throw new NotFoundException({
412
- code: CATALOG_ERROR_CODES.BUNDLE_NOT_FOUND,
511
+ code: CATALOG_ERROR_CODES2.BUNDLE_NOT_FOUND,
413
512
  message: `Bundle '${data.bundleId}' not found`,
414
513
  params: {
415
514
  bundleId: data.bundleId
@@ -418,8 +517,8 @@ var BundlesService = class _BundlesService {
418
517
  }
419
518
  const existingDraft = await this.repo.findCurrentDraft(data.bundleId);
420
519
  if (existingDraft) {
421
- throw new UnprocessableEntityException({
422
- code: CATALOG_ERROR_CODES.BUNDLE_DRAFT_ALREADY_EXISTS,
520
+ throw new UnprocessableEntityException2({
521
+ code: CATALOG_ERROR_CODES2.BUNDLE_DRAFT_ALREADY_EXISTS,
423
522
  message: `Bundle '${bundle.bundleKey}' already has a draft version v${existingDraft.version}; publish or discard it first`,
424
523
  params: {
425
524
  bundleKey: bundle.bundleKey,
@@ -438,7 +537,7 @@ var BundlesService = class _BundlesService {
438
537
  features: data.features,
439
538
  quotas: data.quotas ?? {},
440
539
  compatibility: data.compatibility
441
- }, bundle.projectKey);
540
+ });
442
541
  this.gateOrPass(warnings);
443
542
  const bundleVersion = await this.repo.createDraft(data);
444
543
  return {
@@ -458,7 +557,7 @@ var BundlesService = class _BundlesService {
458
557
  const existing = await this.repo.findVersionById(versionId);
459
558
  if (!existing) {
460
559
  throw new NotFoundException({
461
- code: CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
560
+ code: CATALOG_ERROR_CODES2.BUNDLE_VERSION_NOT_FOUND,
462
561
  message: `BundleVersion '${versionId}' not found`,
463
562
  params: {
464
563
  bundleVersionId: versionId
@@ -470,8 +569,8 @@ var BundlesService = class _BundlesService {
470
569
  ]);
471
570
  const { editable } = isVersionEditable(annotated);
472
571
  if (!editable) {
473
- throw new UnprocessableEntityException({
474
- code: CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_EDITABLE,
572
+ throw new UnprocessableEntityException2({
573
+ code: CATALOG_ERROR_CODES2.BUNDLE_VERSION_NOT_EDITABLE,
475
574
  message: `BundleVersion '${versionId}' is not editable. Only drafts and published versions are editable that are latest-in-chain, bind no subscription yet, and whose validFrom lies in the future.`,
476
575
  params: {
477
576
  versionId
@@ -483,8 +582,7 @@ var BundlesService = class _BundlesService {
483
582
  quotas: data.quotas ?? existing.quotas,
484
583
  compatibility: data.compatibility ?? existing.compatibility
485
584
  };
486
- const bundle = await this.repo.findById(existing.bundleId);
487
- const warnings = await this.runStrictCheck(merged, bundle?.projectKey ?? null);
585
+ const warnings = await this.runStrictCheck(merged);
488
586
  this.gateOrPass(warnings);
489
587
  await this.assertBundleVersionWindowUpdate(existing, data);
490
588
  const bundleVersion = await this.repo.updateDraft(versionId, data);
@@ -507,7 +605,7 @@ var BundlesService = class _BundlesService {
507
605
  const draft = await this.repo.findVersionById(versionId);
508
606
  if (!draft) {
509
607
  throw new NotFoundException({
510
- code: CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
608
+ code: CATALOG_ERROR_CODES2.BUNDLE_VERSION_NOT_FOUND,
511
609
  message: `BundleVersion '${versionId}' not found`,
512
610
  params: {
513
611
  bundleVersionId: versionId
@@ -515,100 +613,44 @@ var BundlesService = class _BundlesService {
515
613
  });
516
614
  }
517
615
  if (draft.publishedAt !== null) {
518
- throw new UnprocessableEntityException({
519
- code: CATALOG_ERROR_CODES.BUNDLE_VERSION_ALREADY_PUBLISHED,
616
+ throw new UnprocessableEntityException2({
617
+ code: CATALOG_ERROR_CODES2.BUNDLE_VERSION_ALREADY_PUBLISHED,
520
618
  message: `BundleVersion '${versionId}' is already published`,
521
619
  params: {
522
620
  versionId
523
621
  }
524
622
  });
525
623
  }
526
- const bundle = await this.repo.findById(draft.bundleId);
527
624
  const warnings = await this.runStrictCheck({
528
625
  features: draft.features,
529
626
  quotas: draft.quotas,
530
627
  compatibility: draft.compatibility
531
- }, bundle?.projectKey ?? null);
628
+ });
532
629
  this.gateOrPass(warnings);
630
+ const hasAnyPrice = draft.monthlyNet != null || draft.yearlyNet != null || (draft.pricingOverrides ?? []).some((o) => o.monthlyNet != null || o.yearlyNet != null);
631
+ if (!hasAnyPrice) {
632
+ throw new UnprocessableEntityException2({
633
+ code: CATALOG_ERROR_CODES2.BUNDLE_VERSION_NO_PRICE,
634
+ message: "A bundle version cannot be published without a price: neither a base price nor any plan override resolves one, so a tenant booking it would be charged nothing for what they receive.",
635
+ params: {
636
+ versionId
637
+ }
638
+ });
639
+ }
533
640
  if (!publishMeta.allowZeroPrice) {
534
641
  const explicitZero = /* @__PURE__ */ __name((v) => v !== null && v !== void 0 && Number.parseFloat(String(v)) <= 0, "explicitZero");
535
642
  if (explicitZero(draft.monthlyNet) || explicitZero(draft.yearlyNet)) {
536
- throw new UnprocessableEntityException({
537
- code: CATALOG_ERROR_CODES.BUNDLE_VERSION_ZERO_PRICE,
643
+ throw new UnprocessableEntityException2({
644
+ code: CATALOG_ERROR_CODES2.BUNDLE_VERSION_ZERO_PRICE,
538
645
  message: "A bundle version cannot be published with an explicit price of 0.00 (guard against seed placeholders). For free bundles leave it null, or set allowZeroPrice.",
539
646
  monthlyNet: draft.monthlyNet,
540
647
  yearlyNet: draft.yearlyNet
541
648
  });
542
649
  }
543
650
  }
651
+ await this.refuseUnsellableToACompatiblePlan(draft, versionId);
544
652
  const previous = await this.repo.findLatestLive(draft.bundleId);
545
- const validFromInput = publishMeta.validFrom ?? draft.validFrom;
546
- if (!validFromInput) {
547
- throw new UnprocessableEntityException({
548
- code: CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_REQUIRED,
549
- message: "validFrom must be set when publishing (on the draft or the publish call).."
550
- });
551
- }
552
- const validFrom = new Date(validFromInput);
553
- if (Number.isNaN(validFrom.getTime())) {
554
- throw new UnprocessableEntityException({
555
- code: CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_INVALID,
556
- message: `validFrom '${validFromInput}' is not a valid date`,
557
- params: {
558
- validFrom: validFromInput
559
- }
560
- });
561
- }
562
- if (previous?.validFrom) {
563
- const prevFrom = new Date(previous.validFrom);
564
- if (validFrom <= prevFrom) {
565
- throw new UnprocessableEntityException({
566
- code: CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS,
567
- message: `validFrom (${validFrom.toISOString()}) must be strictly after the validFrom of the previous version (${previous.validFrom}).`,
568
- params: {
569
- validFrom: validFrom.toISOString(),
570
- previousValidFrom: previous.validFrom
571
- }
572
- });
573
- }
574
- if (previous.validUntil) {
575
- const prevUntil = new Date(previous.validUntil);
576
- const dayMs = 24 * 60 * 60 * 1e3;
577
- const requiredStart = new Date(prevUntil.getTime() + dayMs);
578
- if (validFrom.getTime() !== requiredStart.getTime()) {
579
- throw new UnprocessableEntityException({
580
- code: CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_NOT_GAPLESS,
581
- message: `The predecessor has validUntil=${previous.validUntil.slice(0, 10)} \u2014 the successor must start seamlessly on the next day (${requiredStart.toISOString().slice(0, 10)}). Received: ${validFrom.toISOString().slice(0, 10)}.`,
582
- params: {
583
- received: validFrom.toISOString().slice(0, 10)
584
- },
585
- requiredValidFrom: requiredStart.toISOString().slice(0, 10),
586
- previousValidUntil: previous.validUntil
587
- });
588
- }
589
- }
590
- }
591
- const validUntilInput = publishMeta.validUntil !== void 0 ? publishMeta.validUntil : draft.validUntil;
592
- const validUntil = validUntilInput ? new Date(validUntilInput) : null;
593
- if (validUntil && Number.isNaN(validUntil.getTime())) {
594
- throw new UnprocessableEntityException({
595
- code: CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_UNTIL_INVALID,
596
- message: `validUntil '${validUntilInput}' is not a valid date`,
597
- params: {
598
- validUntil: validUntilInput
599
- }
600
- });
601
- }
602
- if (validUntil && validUntil <= validFrom) {
603
- throw new UnprocessableEntityException({
604
- code: CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_UNTIL_BEFORE_FROM,
605
- message: `validUntil (${validUntil.toISOString()}) must be strictly after validFrom (${validFrom.toISOString()}).`,
606
- params: {
607
- validUntil: validUntil.toISOString(),
608
- validFrom: validFrom.toISOString()
609
- }
610
- });
611
- }
653
+ const { validFrom, validUntil } = resolveValidityWindow(publishMeta, draft, previous, BUNDLE_VERSION_WINDOW_CODES);
612
654
  const diff = previous ? classifyBundleVersionDiff({
613
655
  features: previous.features,
614
656
  quotas: previous.quotas,
@@ -624,8 +666,8 @@ var BundlesService = class _BundlesService {
624
666
  nonRegressive: true
625
667
  };
626
668
  if (!diff.nonRegressive && !publishMeta.forceRegressive) {
627
- throw new UnprocessableEntityException({
628
- code: CATALOG_ERROR_CODES.BUNDLE_VERSION_REGRESSION,
669
+ throw new UnprocessableEntityException2({
670
+ code: CATALOG_ERROR_CODES2.BUNDLE_VERSION_REGRESSION,
629
671
  message: "This bundle version is regressive (feature removed / quota lowered / price raised). Publishing requires an explicit `forceRegressive: true` (UI confirmation dialog with MFA).",
630
672
  changes: diff.changes
631
673
  });
@@ -645,27 +687,71 @@ var BundlesService = class _BundlesService {
645
687
  // =========================================================================
646
688
  // Helper
647
689
  // =========================================================================
648
- async runStrictCheck(draft, projectKey) {
690
+ async runStrictCheck(draft) {
649
691
  const snapshot = resolveDiscoverySnapshot(this.snapshot, this.scanner);
650
692
  if (!snapshot) return [];
651
693
  const planIds = draft.compatibility?.planIds ?? [];
652
694
  let knownPlanKeys = null;
653
- if (planIds.length > 0 && this.planRepo && projectKey) {
654
- const all = await this.planRepo.list({
655
- projectKey
656
- });
695
+ if (planIds.length > 0 && this.planRepo) {
696
+ const all = await this.planRepo.list({});
657
697
  knownPlanKeys = new Set(all.map((p) => p.planKey));
658
698
  }
659
- const approved = await loadApprovedCatalogKeys(this.catalogEntries, snapshot.app.key);
699
+ const approved = await loadApprovedCatalogKeys(this.catalogEntries);
660
700
  return validateBundleDraft(draft, snapshot, knownPlanKeys, this.marketedOnly, approved);
661
701
  }
702
+ /**
703
+ * Refuses a version that a plan it is offered to could not actually buy.
704
+ *
705
+ * "Some price resolves" is not the same as "this plan, in the rhythm it is
706
+ * sold in, resolves one". A bundle priced monthly only, offered to a plan
707
+ * that is sold yearly, passes the first test and fails the second — and the
708
+ * failure surfaces at a tenant's checkout rather than at the operator's
709
+ * desk, which is the wrong end of the mistake.
710
+ *
711
+ * The expectation comes from the catalog, not from a list: the plans are
712
+ * whichever the compatibility declares (empty = every plan), and the cycles
713
+ * are whichever each plan version carries a price for. Add a third cycle to
714
+ * the catalog one day and this asks about it without being told.
715
+ *
716
+ * Skipped where no plan repository is bound — a consumer without one has no
717
+ * catalog to derive from, and guessing would refuse valid bundles.
718
+ */
719
+ async refuseUnsellableToACompatiblePlan(draft, versionId) {
720
+ if (!this.planRepo?.listVersions) return;
721
+ const declared = draft.compatibility?.planIds ?? [];
722
+ const plans = await this.planRepo.list({
723
+ onlyPublished: true
724
+ });
725
+ const offeredTo = plans.filter((plan) => declared.length === 0 || declared.includes(plan.planKey));
726
+ for (const plan of offeredTo) {
727
+ const live = await this.liveVersionOf(plan.planKey);
728
+ if (!live) continue;
729
+ for (const cycle of cyclesSoldFor(live)) {
730
+ if (resolveBundlePriceNet(draft, plan.planKey, cycle) !== null) continue;
731
+ throw new UnprocessableEntityException2({
732
+ code: CATALOG_ERROR_CODES2.BUNDLE_VERSION_NOT_PRICED_FOR_PLAN,
733
+ message: `This bundle is offered to plan '${plan.planKey}', which is sold ${cycle.toLowerCase()}, and no ${cycle.toLowerCase()} price resolves for it \u2014 neither a base price nor a plan override. Price it, or take that plan out of its compatibility.`,
734
+ params: {
735
+ versionId,
736
+ planKey: plan.planKey,
737
+ billingCycle: cycle
738
+ }
739
+ });
740
+ }
741
+ }
742
+ }
743
+ /** The live version of a plan, or null where the port cannot answer. */
744
+ async liveVersionOf(planKey) {
745
+ const versions = await this.planRepo?.listVersions?.(planKey) ?? [];
746
+ return versions.find((version) => version.publishedAt && !version.supersededAt) ?? null;
747
+ }
662
748
  gateOrPass(warnings) {
663
749
  const blocking = blockingStrictModeWarnings(warnings);
664
750
  if (blocking.length === 0) return;
665
751
  const hasHardBlockingWarning = blocking.some((w) => w.code === "BUNDLE_PLAN_KEY_UNKNOWN");
666
752
  if (this.mode === "blocking" || hasHardBlockingWarning) {
667
- throw new UnprocessableEntityException({
668
- code: CATALOG_ERROR_CODES.STRICT_MODE_VIOLATIONS,
753
+ throw new UnprocessableEntityException2({
754
+ code: CATALOG_ERROR_CODES2.STRICT_MODE_VIOLATIONS,
669
755
  message: "The strict-mode check found drift against the discovery snapshot.",
670
756
  warnings
671
757
  });
@@ -678,8 +764,8 @@ var BundlesService = class _BundlesService {
678
764
  const validUntil = this.parseOptionalBundleDate(validUntilInput, "validUntil", "BUNDLE_VERSION_VALID_UNTIL_INVALID");
679
765
  if (!validFromInput) {
680
766
  if (existing.publishedAt !== null) {
681
- throw new UnprocessableEntityException({
682
- code: CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_REQUIRED,
767
+ throw new UnprocessableEntityException2({
768
+ code: CATALOG_ERROR_CODES2.BUNDLE_VERSION_VALID_FROM_REQUIRED,
683
769
  message: "Published bundle versions must keep a validFrom. Set a new future date, or edit the draft before publishing."
684
770
  });
685
771
  }
@@ -687,8 +773,8 @@ var BundlesService = class _BundlesService {
687
773
  }
688
774
  const validFrom = this.parseRequiredBundleDate(validFromInput, "validFrom", "BUNDLE_VERSION_VALID_FROM_INVALID");
689
775
  if (existing.publishedAt !== null && validFrom.getTime() <= Date.now()) {
690
- throw new UnprocessableEntityException({
691
- code: CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_NOT_FUTURE,
776
+ throw new UnprocessableEntityException2({
777
+ code: CATALOG_ERROR_CODES2.BUNDLE_VERSION_VALID_FROM_NOT_FUTURE,
692
778
  message: `validFrom (${validFrom.toISOString()}) must, for a published-but-future bundle version, still lie in the future.`,
693
779
  params: {
694
780
  validFrom: validFrom.toISOString()
@@ -696,8 +782,8 @@ var BundlesService = class _BundlesService {
696
782
  });
697
783
  }
698
784
  if (validUntil && validUntil <= validFrom) {
699
- throw new UnprocessableEntityException({
700
- code: CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_UNTIL_BEFORE_FROM,
785
+ throw new UnprocessableEntityException2({
786
+ code: CATALOG_ERROR_CODES2.BUNDLE_VERSION_VALID_UNTIL_BEFORE_FROM,
701
787
  message: `validUntil (${validUntil.toISOString()}) must be strictly after validFrom (${validFrom.toISOString()}).`,
702
788
  params: {
703
789
  validUntil: validUntil.toISOString(),
@@ -710,8 +796,8 @@ var BundlesService = class _BundlesService {
710
796
  const previousValidFrom = new Date(previous.validFrom);
711
797
  if (Number.isNaN(previousValidFrom.getTime())) return;
712
798
  if (validFrom <= previousValidFrom) {
713
- throw new UnprocessableEntityException({
714
- code: CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS,
799
+ throw new UnprocessableEntityException2({
800
+ code: CATALOG_ERROR_CODES2.BUNDLE_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS,
715
801
  message: `validFrom (${validFrom.toISOString()}) must be strictly after the validFrom of the previous version (${previous.validFrom}).`,
716
802
  params: {
717
803
  validFrom: validFrom.toISOString(),
@@ -725,8 +811,8 @@ var BundlesService = class _BundlesService {
725
811
  const dayMs = 24 * 60 * 60 * 1e3;
726
812
  const requiredStart = new Date(previousValidUntil.getTime() + dayMs);
727
813
  if (validFrom.getTime() !== requiredStart.getTime()) {
728
- throw new UnprocessableEntityException({
729
- code: CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_NOT_GAPLESS,
814
+ throw new UnprocessableEntityException2({
815
+ code: CATALOG_ERROR_CODES2.BUNDLE_VERSION_VALID_FROM_NOT_GAPLESS,
730
816
  message: `The predecessor has validUntil=${previous.validUntil.slice(0, 10)} \u2014 the successor must start seamlessly on the next day (${requiredStart.toISOString().slice(0, 10)}). Received: ${validFrom.toISOString().slice(0, 10)}.`,
731
817
  params: {
732
818
  received: validFrom.toISOString().slice(0, 10)
@@ -747,7 +833,7 @@ var BundlesService = class _BundlesService {
747
833
  parseRequiredBundleDate(input, field, code) {
748
834
  const date = new Date(input);
749
835
  if (Number.isNaN(date.getTime())) {
750
- throw new UnprocessableEntityException({
836
+ throw new UnprocessableEntityException2({
751
837
  code,
752
838
  message: `${field} '${input}' is not a valid date`,
753
839
  // Keyed by the field name so these params match the publish
@@ -789,7 +875,7 @@ BundlesService = _ts_decorate([
789
875
 
790
876
  // src/catalog/marketing-projections.service.ts
791
877
  import { ConflictException, Inject as Inject2, Injectable as Injectable2, NotFoundException as NotFoundException2 } from "@nestjs/common";
792
- import { CATALOG_ERROR_CODES as CATALOG_ERROR_CODES2 } from "@saasicat/core";
878
+ import { CATALOG_ERROR_CODES as CATALOG_ERROR_CODES3 } from "@saasicat/core";
793
879
  function _ts_decorate2(decorators, target, key, desc) {
794
880
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
795
881
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -822,7 +908,7 @@ var MarketingProjectionsService = class {
822
908
  const row = await this.repo.findById(id);
823
909
  if (!row) {
824
910
  throw new NotFoundException2({
825
- code: CATALOG_ERROR_CODES2.MARKETING_PROJECTION_NOT_FOUND,
911
+ code: CATALOG_ERROR_CODES3.MARKETING_PROJECTION_NOT_FOUND,
826
912
  message: `MarketingProjection '${id}' not found`,
827
913
  params: {
828
914
  projectionId: id
@@ -836,7 +922,7 @@ var MarketingProjectionsService = class {
836
922
  const existing = await this.repo.findByTarget(data.targetType, data.targetVersionId, locale);
837
923
  if (existing) {
838
924
  throw new ConflictException({
839
- code: CATALOG_ERROR_CODES2.MARKETING_PROJECTION_ALREADY_EXISTS,
925
+ code: CATALOG_ERROR_CODES3.MARKETING_PROJECTION_ALREADY_EXISTS,
840
926
  message: `Marketing projection for ${data.targetType}/${data.targetVersionId}/${locale} already exists \u2014 use PATCH to edit it`,
841
927
  params: {
842
928
  targetType: data.targetType,
@@ -854,7 +940,7 @@ var MarketingProjectionsService = class {
854
940
  const existing = await this.repo.findById(id);
855
941
  if (!existing) {
856
942
  throw new NotFoundException2({
857
- code: CATALOG_ERROR_CODES2.MARKETING_PROJECTION_NOT_FOUND,
943
+ code: CATALOG_ERROR_CODES3.MARKETING_PROJECTION_NOT_FOUND,
858
944
  message: `MarketingProjection '${id}' not found`,
859
945
  params: {
860
946
  projectionId: id
@@ -867,7 +953,7 @@ var MarketingProjectionsService = class {
867
953
  const existing = await this.repo.findById(id);
868
954
  if (!existing) {
869
955
  throw new NotFoundException2({
870
- code: CATALOG_ERROR_CODES2.MARKETING_PROJECTION_NOT_FOUND,
956
+ code: CATALOG_ERROR_CODES3.MARKETING_PROJECTION_NOT_FOUND,
871
957
  message: `MarketingProjection '${id}' not found`,
872
958
  params: {
873
959
  projectionId: id
@@ -909,8 +995,8 @@ function keyListSegment(name, keys) {
909
995
  __name(keyListSegment, "keyListSegment");
910
996
 
911
997
  // src/catalog/catalog-entries.service.ts
912
- import { Inject as Inject3, Injectable as Injectable3, Logger as Logger2, NotFoundException as NotFoundException3, Optional as Optional2, UnprocessableEntityException as UnprocessableEntityException2 } from "@nestjs/common";
913
- import { CATALOG_ERROR_CODES as CATALOG_ERROR_CODES3 } from "@saasicat/core";
998
+ import { Inject as Inject3, Injectable as Injectable3, Logger as Logger2, NotFoundException as NotFoundException3, Optional as Optional2, UnprocessableEntityException as UnprocessableEntityException3 } from "@nestjs/common";
999
+ import { CATALOG_ERROR_CODES as CATALOG_ERROR_CODES4 } from "@saasicat/core";
914
1000
  function _ts_decorate3(decorators, target, key, desc) {
915
1001
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
916
1002
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -1009,21 +1095,18 @@ var CatalogEntriesService = class _CatalogEntriesService {
1009
1095
  this.logger.error(`Discovery auto-sync on boot failed (${snapshot.app.key}) \u2014 the catalog may be stale; re-sync manually via POST /admin/catalog/discovery/sync`, err instanceof Error ? err.stack : String(err));
1010
1096
  }
1011
1097
  }
1012
- listCapabilities(projectKey, codeStatus) {
1098
+ listCapabilities(codeStatus) {
1013
1099
  return this.repo.listCapabilities({
1014
- projectKey,
1015
1100
  codeStatus
1016
1101
  });
1017
1102
  }
1018
- listFeatures(projectKey, discoveryStatus) {
1103
+ listFeatures(discoveryStatus) {
1019
1104
  return this.repo.listFeatures({
1020
- projectKey,
1021
1105
  discoveryStatus
1022
1106
  });
1023
1107
  }
1024
- listQuotas(projectKey, discoveryStatus) {
1108
+ listQuotas(discoveryStatus) {
1025
1109
  return this.repo.listQuotas({
1026
- projectKey,
1027
1110
  discoveryStatus
1028
1111
  });
1029
1112
  }
@@ -1033,38 +1116,36 @@ var CatalogEntriesService = class _CatalogEntriesService {
1033
1116
  * snapshot, `pending` (revoke/reactivate) deletes it, `outdated`/
1034
1117
  * `obsolete` keep the last approval as history.
1035
1118
  */
1036
- async reviewFeature(projectKey, featureKey, data, reviewedBy) {
1037
- const existing = await this.repo.findFeature(projectKey, featureKey);
1119
+ async reviewFeature(featureKey, data, reviewedBy) {
1120
+ const existing = await this.repo.findFeature(featureKey);
1038
1121
  if (!existing) {
1039
1122
  throw new NotFoundException3({
1040
- code: CATALOG_ERROR_CODES3.FEATURE_NOT_FOUND,
1041
- message: `Feature '${featureKey}' not found in project '${projectKey}'`,
1123
+ code: CATALOG_ERROR_CODES4.FEATURE_NOT_FOUND,
1124
+ message: `Feature '${featureKey}' not found`,
1042
1125
  params: {
1043
- featureKey,
1044
- projectKey
1126
+ featureKey
1045
1127
  }
1046
1128
  });
1047
1129
  }
1048
- return this.repo.setFeatureReview(projectKey, featureKey, this.resolveReviewUpdate(existing, data.discoveryStatus, reviewedBy, (snapshot) => featureApprovalSignature(featureKey, snapshot)));
1130
+ return this.repo.setFeatureReview(featureKey, this.resolveReviewUpdate(existing, data.discoveryStatus, reviewedBy, (snapshot) => featureApprovalSignature(featureKey, snapshot)));
1049
1131
  }
1050
1132
  /** Approval transition of a quota — same state machine as features (#20). */
1051
- async reviewQuota(projectKey, quotaKey, data, reviewedBy) {
1052
- const existing = await this.repo.findQuota(projectKey, quotaKey);
1133
+ async reviewQuota(quotaKey, data, reviewedBy) {
1134
+ const existing = await this.repo.findQuota(quotaKey);
1053
1135
  if (!existing) {
1054
1136
  throw new NotFoundException3({
1055
- code: CATALOG_ERROR_CODES3.QUOTA_NOT_FOUND,
1056
- message: `Quota '${quotaKey}' not found in project '${projectKey}'`,
1137
+ code: CATALOG_ERROR_CODES4.QUOTA_NOT_FOUND,
1138
+ message: `Quota '${quotaKey}' not found`,
1057
1139
  params: {
1058
- quotaKey,
1059
- projectKey
1140
+ quotaKey
1060
1141
  }
1061
1142
  });
1062
1143
  }
1063
- return this.repo.setQuotaReview(projectKey, quotaKey, this.resolveReviewUpdate(existing, data.discoveryStatus, reviewedBy, (snapshot) => {
1144
+ return this.repo.setQuotaReview(quotaKey, this.resolveReviewUpdate(existing, data.discoveryStatus, reviewedBy, (snapshot) => {
1064
1145
  const discovered = snapshot.quotas.find((q) => q.quotaKey === quotaKey);
1065
1146
  if (!discovered) {
1066
- throw new UnprocessableEntityException2({
1067
- code: CATALOG_ERROR_CODES3.QUOTA_NOT_IN_DISCOVERY_SNAPSHOT,
1147
+ throw new UnprocessableEntityException3({
1148
+ code: CATALOG_ERROR_CODES4.QUOTA_NOT_IN_DISCOVERY_SNAPSHOT,
1068
1149
  message: `Quota '${quotaKey}' is not in the discovery snapshot \u2014 a quota that is not declared in code cannot be approved`,
1069
1150
  params: {
1070
1151
  quotaKey
@@ -1076,8 +1157,8 @@ var CatalogEntriesService = class _CatalogEntriesService {
1076
1157
  }
1077
1158
  resolveReviewUpdate(existing, target, reviewedBy, signatureOf) {
1078
1159
  if (!REVIEW_TRANSITIONS[existing.discoveryStatus].includes(target)) {
1079
- throw new UnprocessableEntityException2({
1080
- code: CATALOG_ERROR_CODES3.DISCOVERY_STATUS_TRANSITION_INVALID,
1160
+ throw new UnprocessableEntityException3({
1161
+ code: CATALOG_ERROR_CODES4.DISCOVERY_STATUS_TRANSITION_INVALID,
1081
1162
  message: `Transition '${existing.discoveryStatus}' \u2192 '${target}' is not allowed`,
1082
1163
  params: {
1083
1164
  from: existing.discoveryStatus,
@@ -1088,8 +1169,8 @@ var CatalogEntriesService = class _CatalogEntriesService {
1088
1169
  if (target === "approved") {
1089
1170
  const snapshot = resolveDiscoverySnapshot(this.snapshot, this.scanner);
1090
1171
  if (!snapshot) {
1091
- throw new UnprocessableEntityException2({
1092
- code: CATALOG_ERROR_CODES3.DISCOVERY_NOT_INITIALIZED,
1172
+ throw new UnprocessableEntityException3({
1173
+ code: CATALOG_ERROR_CODES4.DISCOVERY_NOT_INITIALIZED,
1093
1174
  message: "Approval requires a discovery snapshot \u2014 discovery is not initialised (#25)"
1094
1175
  });
1095
1176
  }
@@ -1108,30 +1189,27 @@ var CatalogEntriesService = class _CatalogEntriesService {
1108
1189
  approvedSignature: keepApproval ? existing.approvedSignature : null
1109
1190
  };
1110
1191
  }
1111
- setFeatureI18n(projectKey, featureKey, i18n) {
1112
- return this.repo.setFeatureI18n(projectKey, featureKey, i18n);
1192
+ setFeatureI18n(featureKey, i18n) {
1193
+ return this.repo.setFeatureI18n(featureKey, i18n);
1113
1194
  }
1114
- setQuotaI18n(projectKey, quotaKey, i18n) {
1115
- return this.repo.setQuotaI18n(projectKey, quotaKey, i18n);
1195
+ setQuotaI18n(quotaKey, i18n) {
1196
+ return this.repo.setQuotaI18n(quotaKey, i18n);
1116
1197
  }
1117
1198
  /** Sets the editable default-locale label/description of a feature. */
1118
- setFeatureBase(projectKey, featureKey, data) {
1119
- return this.repo.setFeatureBase(projectKey, featureKey, data);
1199
+ setFeatureBase(featureKey, data) {
1200
+ return this.repo.setFeatureBase(featureKey, data);
1120
1201
  }
1121
1202
  /** Sets the editable default-locale label/description of a quota. */
1122
- setQuotaBase(projectKey, quotaKey, data) {
1123
- return this.repo.setQuotaBase(projectKey, quotaKey, data);
1203
+ setQuotaBase(quotaKey, data) {
1204
+ return this.repo.setQuotaBase(quotaKey, data);
1124
1205
  }
1125
1206
  /**
1126
1207
  * Synchronizes the catalog entries from a discovery snapshot.
1127
1208
  * Idempotent — can run against the same snapshot any number of times.
1128
1209
  */
1129
1210
  async syncFromSnapshot(snapshot) {
1130
- const projectKey = snapshot.app.key;
1131
1211
  const nowIso = (/* @__PURE__ */ new Date()).toISOString();
1132
- const existingCaps = await this.repo.listCapabilities({
1133
- projectKey
1134
- });
1212
+ const existingCaps = await this.repo.listCapabilities({});
1135
1213
  const capByKey = new Map(existingCaps.map((c) => [
1136
1214
  c.capabilityKey,
1137
1215
  c
@@ -1144,7 +1222,6 @@ var CatalogEntriesService = class _CatalogEntriesService {
1144
1222
  const existing = capByKey.get(cap.capabilityKey);
1145
1223
  if (!existing || existing.codeStatus === "retired") capDiscovered++;
1146
1224
  await this.repo.upsertCapability({
1147
- projectKey,
1148
1225
  capabilityKey: cap.capabilityKey,
1149
1226
  label: cap.label ?? cap.capabilityKey,
1150
1227
  description: existing?.description ?? null,
@@ -1159,10 +1236,8 @@ var CatalogEntriesService = class _CatalogEntriesService {
1159
1236
  reason: cap.reason ?? existing?.reason ?? null
1160
1237
  });
1161
1238
  }
1162
- const capRetired = await this.repo.retireMissing(projectKey, "capability", presentCaps);
1163
- const existingFeatures = await this.repo.listFeatures({
1164
- projectKey
1165
- });
1239
+ const capRetired = await this.repo.retireMissing("capability", presentCaps);
1240
+ const existingFeatures = await this.repo.listFeatures({});
1166
1241
  const featureByKey = new Map(existingFeatures.map((f) => [
1167
1242
  f.featureKey,
1168
1243
  f
@@ -1181,7 +1256,6 @@ var CatalogEntriesService = class _CatalogEntriesService {
1181
1256
  const meta = this.featureUiRegistry?.[feature.featureKey];
1182
1257
  const hasCuratedLabel = existing != null && existing.label !== feature.featureKey;
1183
1258
  await this.repo.upsertFeature({
1184
- projectKey,
1185
1259
  featureKey: feature.featureKey,
1186
1260
  label: hasCuratedLabel ? existing.label : meta?.label ?? feature.featureKey,
1187
1261
  description: existing?.description ?? meta?.description ?? null,
@@ -1191,22 +1265,20 @@ var CatalogEntriesService = class _CatalogEntriesService {
1191
1265
  core: meta?.core ?? false
1192
1266
  });
1193
1267
  if (meta?.icon && !existing?.icon) {
1194
- await this.repo.setFeatureBase(projectKey, feature.featureKey, {
1268
+ await this.repo.setFeatureBase(feature.featureKey, {
1195
1269
  icon: meta.icon
1196
1270
  });
1197
1271
  }
1198
1272
  }
1199
- const featureRetired = await this.repo.retireMissing(projectKey, "feature", presentFeatures);
1200
- const featureReplaced = await this.applySuccessorPointers("feature", projectKey, existingFeatures.map((f) => ({
1273
+ const featureRetired = await this.repo.retireMissing("feature", presentFeatures);
1274
+ const featureReplaced = await this.applySuccessorPointers("feature", existingFeatures.map((f) => ({
1201
1275
  key: f.featureKey,
1202
1276
  successorKey: f.successorKey
1203
1277
  })), new Set(presentFeatures), buildSuccessorIndex(snapshot.features.map((f) => ({
1204
1278
  key: f.featureKey,
1205
1279
  replaces: f.replaces
1206
1280
  }))));
1207
- const existingQuotas = await this.repo.listQuotas({
1208
- projectKey
1209
- });
1281
+ const existingQuotas = await this.repo.listQuotas({});
1210
1282
  const quotaByKey = new Map(existingQuotas.map((q) => [
1211
1283
  q.quotaKey,
1212
1284
  q
@@ -1223,7 +1295,6 @@ var CatalogEntriesService = class _CatalogEntriesService {
1223
1295
  quotaOutdated++;
1224
1296
  }
1225
1297
  await this.repo.upsertQuota({
1226
- projectKey,
1227
1298
  quotaKey: quota.quotaKey,
1228
1299
  label: quota.label,
1229
1300
  description: existing?.description ?? null,
@@ -1235,8 +1306,8 @@ var CatalogEntriesService = class _CatalogEntriesService {
1235
1306
  replaces: quota.replaces ?? []
1236
1307
  });
1237
1308
  }
1238
- const quotaRetired = await this.repo.retireMissing(projectKey, "quota", presentQuotas);
1239
- const quotaReplaced = await this.applySuccessorPointers("quota", projectKey, existingQuotas.map((q) => ({
1309
+ const quotaRetired = await this.repo.retireMissing("quota", presentQuotas);
1310
+ const quotaReplaced = await this.applySuccessorPointers("quota", existingQuotas.map((q) => ({
1240
1311
  key: q.quotaKey,
1241
1312
  successorKey: q.successorKey
1242
1313
  })), new Set(presentQuotas), buildSuccessorIndex(snapshot.quotas.map((q) => ({
@@ -1273,7 +1344,7 @@ var CatalogEntriesService = class _CatalogEntriesService {
1273
1344
  * the code, an existing pointer is deleted. A missing key
1274
1345
  * without a claimant remains untouched (deleted without replacement).
1275
1346
  */
1276
- async applySuccessorPointers(type, projectKey, existing, presentKeys, claimantsByOldKey) {
1347
+ async applySuccessorPointers(type, existing, presentKeys, claimantsByOldKey) {
1277
1348
  const changes = [];
1278
1349
  for (const entry of existing) {
1279
1350
  const current = entry.successorKey ?? null;
@@ -1302,7 +1373,7 @@ var CatalogEntriesService = class _CatalogEntriesService {
1302
1373
  }
1303
1374
  let replaced = 0;
1304
1375
  for (const change of changes) {
1305
- await setSuccessor(projectKey, change.key, change.successorKey);
1376
+ await setSuccessor(change.key, change.successorKey);
1306
1377
  if (change.successorKey !== null) replaced++;
1307
1378
  }
1308
1379
  return replaced;
@@ -1330,7 +1401,7 @@ CatalogEntriesService = _ts_decorate3([
1330
1401
  ], CatalogEntriesService);
1331
1402
 
1332
1403
  // src/catalog/dto/catalog-entries.dto.ts
1333
- import { IsIn, IsObject, IsOptional, IsString, Matches, MaxLength } from "class-validator";
1404
+ import { IsIn, IsObject, IsOptional, IsString, MaxLength } from "class-validator";
1334
1405
  function _ts_decorate4(decorators, target, key, desc) {
1335
1406
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1336
1407
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -1342,7 +1413,6 @@ function _ts_metadata4(k, v) {
1342
1413
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1343
1414
  }
1344
1415
  __name(_ts_metadata4, "_ts_metadata");
1345
- var PROJECT_KEY_PATTERN = /^[a-z][a-z0-9-]*$/;
1346
1416
  var REVIEW_STATUSES = [
1347
1417
  "pending",
1348
1418
  "approved",
@@ -1359,19 +1429,11 @@ var ListCatalogEntriesQueryDto = class {
1359
1429
  static {
1360
1430
  __name(this, "ListCatalogEntriesQueryDto");
1361
1431
  }
1362
- projectKey;
1363
1432
  /** Filter for features/quotas (approval lifecycle). */
1364
1433
  discoveryStatus;
1365
1434
  /** Filter for capabilities (read-only code facts). */
1366
1435
  codeStatus;
1367
1436
  };
1368
- _ts_decorate4([
1369
- IsString(),
1370
- Matches(PROJECT_KEY_PATTERN, {
1371
- message: "projectKey must be kebab-case"
1372
- }),
1373
- _ts_metadata4("design:type", String)
1374
- ], ListCatalogEntriesQueryDto.prototype, "projectKey", void 0);
1375
1437
  _ts_decorate4([
1376
1438
  IsOptional(),
1377
1439
  IsString(),
@@ -1476,6 +1538,21 @@ function _ts_param4(paramIndex, decorator) {
1476
1538
  }
1477
1539
  __name(_ts_param4, "_ts_param");
1478
1540
  var DEFAULT_CONTEXT = "admin";
1541
+ function defaultActorFromRequest(req) {
1542
+ const request = req;
1543
+ const sid = request.headers?.["x-session-id"];
1544
+ return {
1545
+ userId: request.user?.sub ?? request.user?.id ?? "unknown",
1546
+ email: request.user?.email ?? "unknown",
1547
+ source: "web",
1548
+ context: (Array.isArray(sid) ? sid[0] : sid) ?? DEFAULT_CONTEXT
1549
+ };
1550
+ }
1551
+ __name(defaultActorFromRequest, "defaultActorFromRequest");
1552
+ function actorTagOf(actor) {
1553
+ return `${actor.source}:${actor.email}:${actor.context}`;
1554
+ }
1555
+ __name(actorTagOf, "actorTagOf");
1479
1556
  var WebAuditLogger = class {
1480
1557
  static {
1481
1558
  __name(this, "WebAuditLogger");
@@ -1512,6 +1589,15 @@ var WebAuditLogger = class {
1512
1589
  context
1513
1590
  };
1514
1591
  }
1592
+ /**
1593
+ * The actor behind a request as the audit log writes it —
1594
+ * `web:<email>:<context>` — for a column that records who did something.
1595
+ * The same derivation as the log line, so the two never disagree about who
1596
+ * it was.
1597
+ */
1598
+ actorTagFromRequest(req) {
1599
+ return actorTagOf(this.buildActor(req));
1600
+ }
1515
1601
  /**
1516
1602
  * Best-effort audit log from a web request. Writes nothing and never
1517
1603
  * throws when no AdminAuditService is injected; write errors are
@@ -1580,45 +1666,45 @@ function buildCatalogEntriesController(guards) {
1580
1666
  this.audit = audit;
1581
1667
  }
1582
1668
  listCapabilities(query) {
1583
- return this.service.listCapabilities(query.projectKey, query.codeStatus);
1669
+ return this.service.listCapabilities(query.codeStatus);
1584
1670
  }
1585
1671
  listFeatures(query) {
1586
- return this.service.listFeatures(query.projectKey, query.discoveryStatus);
1672
+ return this.service.listFeatures(query.discoveryStatus);
1587
1673
  }
1588
1674
  listQuotas(query) {
1589
- return this.service.listQuotas(query.projectKey, query.discoveryStatus);
1675
+ return this.service.listQuotas(query.discoveryStatus);
1590
1676
  }
1591
- async reviewFeature(req, featureKey, projectKey, dto) {
1592
- const row = await this.service.reviewFeature(projectKey, featureKey, dto, this.audit?.resolveUserId(req) ?? null);
1593
- await this.audit?.logFromRequest(req, "CatalogFeatureEntry", `${projectKey}:${featureKey}`, "REVIEW_FEATURE", {
1677
+ async reviewFeature(req, featureKey, dto) {
1678
+ const row = await this.service.reviewFeature(featureKey, dto, this.audit?.resolveUserId(req) ?? null);
1679
+ await this.audit?.logFromRequest(req, "CatalogFeatureEntry", featureKey, "REVIEW_FEATURE", {
1594
1680
  discoveryStatus: dto.discoveryStatus
1595
1681
  });
1596
1682
  return row;
1597
1683
  }
1598
- async reviewQuota(req, quotaKey, projectKey, dto) {
1599
- const row = await this.service.reviewQuota(projectKey, quotaKey, dto, this.audit?.resolveUserId(req) ?? null);
1600
- await this.audit?.logFromRequest(req, "CatalogQuotaEntry", `${projectKey}:${quotaKey}`, "REVIEW_QUOTA", {
1684
+ async reviewQuota(req, quotaKey, dto) {
1685
+ const row = await this.service.reviewQuota(quotaKey, dto, this.audit?.resolveUserId(req) ?? null);
1686
+ await this.audit?.logFromRequest(req, "CatalogQuotaEntry", quotaKey, "REVIEW_QUOTA", {
1601
1687
  discoveryStatus: dto.discoveryStatus
1602
1688
  });
1603
1689
  return row;
1604
1690
  }
1605
- async setFeatureI18n(req, featureKey, projectKey, dto) {
1606
- const row = await this.service.setFeatureI18n(projectKey, featureKey, dto.i18n);
1607
- await this.audit?.logFromRequest(req, "CatalogFeatureEntry", `${projectKey}:${featureKey}`, "SET_FEATURE_I18N", {
1691
+ async setFeatureI18n(req, featureKey, dto) {
1692
+ const row = await this.service.setFeatureI18n(featureKey, dto.i18n);
1693
+ await this.audit?.logFromRequest(req, "CatalogFeatureEntry", featureKey, "SET_FEATURE_I18N", {
1608
1694
  locales: Object.keys(dto.i18n ?? {})
1609
1695
  });
1610
1696
  return row;
1611
1697
  }
1612
- async setQuotaI18n(req, quotaKey, projectKey, dto) {
1613
- const row = await this.service.setQuotaI18n(projectKey, quotaKey, dto.i18n);
1614
- await this.audit?.logFromRequest(req, "CatalogQuotaEntry", `${projectKey}:${quotaKey}`, "SET_QUOTA_I18N", {
1698
+ async setQuotaI18n(req, quotaKey, dto) {
1699
+ const row = await this.service.setQuotaI18n(quotaKey, dto.i18n);
1700
+ await this.audit?.logFromRequest(req, "CatalogQuotaEntry", quotaKey, "SET_QUOTA_I18N", {
1615
1701
  locales: Object.keys(dto.i18n ?? {})
1616
1702
  });
1617
1703
  return row;
1618
1704
  }
1619
- async setFeatureBase(req, featureKey, projectKey, dto) {
1620
- const row = await this.service.setFeatureBase(projectKey, featureKey, dto);
1621
- await this.audit?.logFromRequest(req, "CatalogFeatureEntry", `${projectKey}:${featureKey}`, "SET_FEATURE_BASE", {
1705
+ async setFeatureBase(req, featureKey, dto) {
1706
+ const row = await this.service.setFeatureBase(featureKey, dto);
1707
+ await this.audit?.logFromRequest(req, "CatalogFeatureEntry", featureKey, "SET_FEATURE_BASE", {
1622
1708
  label: dto.label,
1623
1709
  description: dto.description,
1624
1710
  icon: dto.icon,
@@ -1626,9 +1712,9 @@ function buildCatalogEntriesController(guards) {
1626
1712
  });
1627
1713
  return row;
1628
1714
  }
1629
- async setQuotaBase(req, quotaKey, projectKey, dto) {
1630
- const row = await this.service.setQuotaBase(projectKey, quotaKey, dto);
1631
- await this.audit?.logFromRequest(req, "CatalogQuotaEntry", `${projectKey}:${quotaKey}`, "SET_QUOTA_BASE", {
1715
+ async setQuotaBase(req, quotaKey, dto) {
1716
+ const row = await this.service.setQuotaBase(quotaKey, dto);
1717
+ await this.audit?.logFromRequest(req, "CatalogQuotaEntry", quotaKey, "SET_QUOTA_BASE", {
1632
1718
  label: dto.label,
1633
1719
  description: dto.description
1634
1720
  });
@@ -1676,13 +1762,11 @@ function buildCatalogEntriesController(guards) {
1676
1762
  Patch("features/:key/review"),
1677
1763
  _ts_param5(0, Req()),
1678
1764
  _ts_param5(1, Param("key")),
1679
- _ts_param5(2, Query("projectKey")),
1680
- _ts_param5(3, Body()),
1765
+ _ts_param5(2, Body()),
1681
1766
  _ts_metadata6("design:type", Function),
1682
1767
  _ts_metadata6("design:paramtypes", [
1683
1768
  Object,
1684
1769
  String,
1685
- String,
1686
1770
  typeof ReviewCatalogEntryDto === "undefined" ? Object : ReviewCatalogEntryDto
1687
1771
  ]),
1688
1772
  _ts_metadata6("design:returntype", Promise)
@@ -1691,13 +1775,11 @@ function buildCatalogEntriesController(guards) {
1691
1775
  Patch("quotas/:key/review"),
1692
1776
  _ts_param5(0, Req()),
1693
1777
  _ts_param5(1, Param("key")),
1694
- _ts_param5(2, Query("projectKey")),
1695
- _ts_param5(3, Body()),
1778
+ _ts_param5(2, Body()),
1696
1779
  _ts_metadata6("design:type", Function),
1697
1780
  _ts_metadata6("design:paramtypes", [
1698
1781
  Object,
1699
1782
  String,
1700
- String,
1701
1783
  typeof ReviewCatalogEntryDto === "undefined" ? Object : ReviewCatalogEntryDto
1702
1784
  ]),
1703
1785
  _ts_metadata6("design:returntype", Promise)
@@ -1706,13 +1788,11 @@ function buildCatalogEntriesController(guards) {
1706
1788
  Patch("features/:key/i18n"),
1707
1789
  _ts_param5(0, Req()),
1708
1790
  _ts_param5(1, Param("key")),
1709
- _ts_param5(2, Query("projectKey")),
1710
- _ts_param5(3, Body()),
1791
+ _ts_param5(2, Body()),
1711
1792
  _ts_metadata6("design:type", Function),
1712
1793
  _ts_metadata6("design:paramtypes", [
1713
1794
  Object,
1714
1795
  String,
1715
- String,
1716
1796
  typeof UpdateCatalogEntryI18nDto === "undefined" ? Object : UpdateCatalogEntryI18nDto
1717
1797
  ]),
1718
1798
  _ts_metadata6("design:returntype", Promise)
@@ -1721,13 +1801,11 @@ function buildCatalogEntriesController(guards) {
1721
1801
  Patch("quotas/:key/i18n"),
1722
1802
  _ts_param5(0, Req()),
1723
1803
  _ts_param5(1, Param("key")),
1724
- _ts_param5(2, Query("projectKey")),
1725
- _ts_param5(3, Body()),
1804
+ _ts_param5(2, Body()),
1726
1805
  _ts_metadata6("design:type", Function),
1727
1806
  _ts_metadata6("design:paramtypes", [
1728
1807
  Object,
1729
1808
  String,
1730
- String,
1731
1809
  typeof UpdateCatalogEntryI18nDto === "undefined" ? Object : UpdateCatalogEntryI18nDto
1732
1810
  ]),
1733
1811
  _ts_metadata6("design:returntype", Promise)
@@ -1736,13 +1814,11 @@ function buildCatalogEntriesController(guards) {
1736
1814
  Patch("features/:key"),
1737
1815
  _ts_param5(0, Req()),
1738
1816
  _ts_param5(1, Param("key")),
1739
- _ts_param5(2, Query("projectKey")),
1740
- _ts_param5(3, Body()),
1817
+ _ts_param5(2, Body()),
1741
1818
  _ts_metadata6("design:type", Function),
1742
1819
  _ts_metadata6("design:paramtypes", [
1743
1820
  Object,
1744
1821
  String,
1745
- String,
1746
1822
  typeof UpdateCatalogEntryBaseDto === "undefined" ? Object : UpdateCatalogEntryBaseDto
1747
1823
  ]),
1748
1824
  _ts_metadata6("design:returntype", Promise)
@@ -1751,13 +1827,11 @@ function buildCatalogEntriesController(guards) {
1751
1827
  Patch("quotas/:key"),
1752
1828
  _ts_param5(0, Req()),
1753
1829
  _ts_param5(1, Param("key")),
1754
- _ts_param5(2, Query("projectKey")),
1755
- _ts_param5(3, Body()),
1830
+ _ts_param5(2, Body()),
1756
1831
  _ts_metadata6("design:type", Function),
1757
1832
  _ts_metadata6("design:paramtypes", [
1758
1833
  Object,
1759
1834
  String,
1760
- String,
1761
1835
  typeof UpdateCatalogEntryBaseDto === "undefined" ? Object : UpdateCatalogEntryBaseDto
1762
1836
  ]),
1763
1837
  _ts_metadata6("design:returntype", Promise)
@@ -1791,7 +1865,47 @@ __name(buildCatalogEntriesController, "buildCatalogEntriesController");
1791
1865
 
1792
1866
  // src/catalog/promotions.service.ts
1793
1867
  import { Inject as Inject6, Injectable as Injectable5, NotFoundException as NotFoundException4 } from "@nestjs/common";
1794
- import { CATALOG_ERROR_CODES as CATALOG_ERROR_CODES4 } from "@saasicat/core";
1868
+ import { CATALOG_ERROR_CODES as CATALOG_ERROR_CODES6 } from "@saasicat/core";
1869
+
1870
+ // src/catalog/promotion-value.ts
1871
+ import { UnprocessableEntityException as UnprocessableEntityException4 } from "@nestjs/common";
1872
+ import { CATALOG_ERROR_CODES as CATALOG_ERROR_CODES5 } from "@saasicat/core";
1873
+ function assertPromotionValue(type, value) {
1874
+ if (takesValue(type, value)) return;
1875
+ throw new UnprocessableEntityException4({
1876
+ code: CATALOG_ERROR_CODES5.PROMOTION_VALUE_INVALID,
1877
+ message: `The value of a '${type}' promotion is not one its type takes: a percentage above 0 and at most 100, an amount above 0, an intro price of at least 0 for a whole number of months, or a whole number of free months.`,
1878
+ params: {
1879
+ type
1880
+ }
1881
+ });
1882
+ }
1883
+ __name(assertPromotionValue, "assertPromotionValue");
1884
+ function takesValue(type, value) {
1885
+ switch (type) {
1886
+ case "percent":
1887
+ return isAmount(value) && value > 0 && value <= 100;
1888
+ case "amount":
1889
+ return isAmount(value) && value > 0;
1890
+ case "intro":
1891
+ return typeof value === "object" && value !== null && isAmount(value.price) && value.price >= 0 && isMonths(value.months);
1892
+ case "freeMonths":
1893
+ return isMonths(value);
1894
+ default:
1895
+ return false;
1896
+ }
1897
+ }
1898
+ __name(takesValue, "takesValue");
1899
+ function isAmount(value) {
1900
+ return typeof value === "number" && Number.isFinite(value);
1901
+ }
1902
+ __name(isAmount, "isAmount");
1903
+ function isMonths(value) {
1904
+ return Number.isInteger(value) && value >= 1;
1905
+ }
1906
+ __name(isMonths, "isMonths");
1907
+
1908
+ // src/catalog/promotions.service.ts
1795
1909
  function _ts_decorate7(decorators, target, key, desc) {
1796
1910
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1797
1911
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -1817,16 +1931,14 @@ var PromotionsService = class {
1817
1931
  constructor(repo) {
1818
1932
  this.repo = repo;
1819
1933
  }
1820
- list(projectKey) {
1821
- return this.repo.list({
1822
- projectKey
1823
- });
1934
+ list() {
1935
+ return this.repo.list();
1824
1936
  }
1825
1937
  async getById(id) {
1826
1938
  const row = await this.repo.findById(id);
1827
1939
  if (!row) {
1828
1940
  throw new NotFoundException4({
1829
- code: CATALOG_ERROR_CODES4.PROMOTION_NOT_FOUND,
1941
+ code: CATALOG_ERROR_CODES6.PROMOTION_NOT_FOUND,
1830
1942
  message: `Promotion '${id}' not found`,
1831
1943
  params: {
1832
1944
  promotionId: id
@@ -1835,27 +1947,29 @@ var PromotionsService = class {
1835
1947
  }
1836
1948
  return row;
1837
1949
  }
1838
- create(data) {
1950
+ async create(data) {
1951
+ assertPromotionValue(data.type, data.value);
1839
1952
  return this.repo.create(data);
1840
1953
  }
1841
1954
  async update(id, data) {
1842
1955
  const existing = await this.repo.findById(id);
1843
1956
  if (!existing) {
1844
1957
  throw new NotFoundException4({
1845
- code: CATALOG_ERROR_CODES4.PROMOTION_NOT_FOUND,
1958
+ code: CATALOG_ERROR_CODES6.PROMOTION_NOT_FOUND,
1846
1959
  message: `Promotion '${id}' not found`,
1847
1960
  params: {
1848
1961
  promotionId: id
1849
1962
  }
1850
1963
  });
1851
1964
  }
1965
+ assertPromotionValue(data.type === void 0 ? existing.type : data.type, data.value === void 0 ? existing.value : data.value);
1852
1966
  return this.repo.update(id, data);
1853
1967
  }
1854
1968
  async delete(id) {
1855
1969
  const existing = await this.repo.findById(id);
1856
1970
  if (!existing) {
1857
1971
  throw new NotFoundException4({
1858
- code: CATALOG_ERROR_CODES4.PROMOTION_NOT_FOUND,
1972
+ code: CATALOG_ERROR_CODES6.PROMOTION_NOT_FOUND,
1859
1973
  message: `Promotion '${id}' not found`,
1860
1974
  params: {
1861
1975
  promotionId: id
@@ -1875,7 +1989,7 @@ PromotionsService = _ts_decorate7([
1875
1989
  ], PromotionsService);
1876
1990
 
1877
1991
  // src/catalog/dto/promotions.dto.ts
1878
- import { IsArray, IsBoolean, IsDefined, IsIn as IsIn2, IsInt, IsObject as IsObject2, IsOptional as IsOptional2, IsString as IsString2, Matches as Matches2, MaxLength as MaxLength2 } from "class-validator";
1992
+ import { IsArray, IsBoolean, IsDefined, IsIn as IsIn2, IsInt, IsObject as IsObject2, IsOptional as IsOptional2, IsString as IsString2, Matches, MaxLength as MaxLength2 } from "class-validator";
1879
1993
  function _ts_decorate8(decorators, target, key, desc) {
1880
1994
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1881
1995
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -1887,7 +2001,6 @@ function _ts_metadata8(k, v) {
1887
2001
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1888
2002
  }
1889
2003
  __name(_ts_metadata8, "_ts_metadata");
1890
- var PROJECT_KEY_PATTERN2 = /^[a-z][a-z0-9-]*$/;
1891
2004
  var ISO_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}/;
1892
2005
  var TYPES = [
1893
2006
  "percent",
@@ -1905,24 +2018,10 @@ var TARGET_TYPES = [
1905
2018
  "BUNDLE",
1906
2019
  "OFFER"
1907
2020
  ];
1908
- var ListPromotionsQueryDto = class {
1909
- static {
1910
- __name(this, "ListPromotionsQueryDto");
1911
- }
1912
- projectKey;
1913
- };
1914
- _ts_decorate8([
1915
- IsString2(),
1916
- Matches2(PROJECT_KEY_PATTERN2, {
1917
- message: "projectKey must be kebab-case"
1918
- }),
1919
- _ts_metadata8("design:type", String)
1920
- ], ListPromotionsQueryDto.prototype, "projectKey", void 0);
1921
2021
  var CreatePromotionDto = class {
1922
2022
  static {
1923
2023
  __name(this, "CreatePromotionDto");
1924
2024
  }
1925
- projectKey;
1926
2025
  internalLabel;
1927
2026
  type;
1928
2027
  /** Type-dependent: number | { price, months }. */
@@ -1939,12 +2038,6 @@ var CreatePromotionDto = class {
1939
2038
  color;
1940
2039
  i18n;
1941
2040
  };
1942
- _ts_decorate8([
1943
- IsString2(),
1944
- Matches2(PROJECT_KEY_PATTERN2),
1945
- MaxLength2(64),
1946
- _ts_metadata8("design:type", String)
1947
- ], CreatePromotionDto.prototype, "projectKey", void 0);
1948
2041
  _ts_decorate8([
1949
2042
  IsString2(),
1950
2043
  MaxLength2(120),
@@ -1981,14 +2074,14 @@ _ts_decorate8([
1981
2074
  ], CreatePromotionDto.prototype, "billingCycle", void 0);
1982
2075
  _ts_decorate8([
1983
2076
  IsString2(),
1984
- Matches2(ISO_DATE_PATTERN, {
2077
+ Matches(ISO_DATE_PATTERN, {
1985
2078
  message: "validFrom must be an ISO date"
1986
2079
  }),
1987
2080
  _ts_metadata8("design:type", String)
1988
2081
  ], CreatePromotionDto.prototype, "validFrom", void 0);
1989
2082
  _ts_decorate8([
1990
2083
  IsString2(),
1991
- Matches2(ISO_DATE_PATTERN, {
2084
+ Matches(ISO_DATE_PATTERN, {
1992
2085
  message: "validTo must be an ISO date"
1993
2086
  }),
1994
2087
  _ts_metadata8("design:type", String)
@@ -2088,13 +2181,13 @@ _ts_decorate8([
2088
2181
  _ts_decorate8([
2089
2182
  IsOptional2(),
2090
2183
  IsString2(),
2091
- Matches2(ISO_DATE_PATTERN),
2184
+ Matches(ISO_DATE_PATTERN),
2092
2185
  _ts_metadata8("design:type", String)
2093
2186
  ], UpdatePromotionDto.prototype, "validFrom", void 0);
2094
2187
  _ts_decorate8([
2095
2188
  IsOptional2(),
2096
2189
  IsString2(),
2097
- Matches2(ISO_DATE_PATTERN),
2190
+ Matches(ISO_DATE_PATTERN),
2098
2191
  _ts_metadata8("design:type", String)
2099
2192
  ], UpdatePromotionDto.prototype, "validTo", void 0);
2100
2193
  _ts_decorate8([
@@ -2136,7 +2229,7 @@ _ts_decorate8([
2136
2229
  ], UpdatePromotionDto.prototype, "i18n", void 0);
2137
2230
 
2138
2231
  // src/catalog/promotions.controller.ts
2139
- import { Body as Body2, Controller as Controller2, Delete, Get as Get2, HttpCode, HttpStatus, Inject as Inject7, Param as Param2, ParseUUIDPipe, Patch as Patch2, Post as Post2, Query as Query2, UseGuards as UseGuards2 } from "@nestjs/common";
2232
+ import { Body as Body2, Controller as Controller2, Delete, Get as Get2, HttpCode, HttpStatus, Inject as Inject7, Param as Param2, ParseUUIDPipe, Patch as Patch2, Post as Post2, UseGuards as UseGuards2 } from "@nestjs/common";
2140
2233
  function _ts_decorate9(decorators, target, key, desc) {
2141
2234
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2142
2235
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -2163,8 +2256,8 @@ function buildPromotionsController(guards) {
2163
2256
  constructor(service) {
2164
2257
  this.service = service;
2165
2258
  }
2166
- list(query) {
2167
- return this.service.list(query.projectKey);
2259
+ list() {
2260
+ return this.service.list();
2168
2261
  }
2169
2262
  getById(id) {
2170
2263
  return this.service.getById(id);
@@ -2181,11 +2274,8 @@ function buildPromotionsController(guards) {
2181
2274
  };
2182
2275
  _ts_decorate9([
2183
2276
  Get2(),
2184
- _ts_param7(0, Query2()),
2185
2277
  _ts_metadata9("design:type", Function),
2186
- _ts_metadata9("design:paramtypes", [
2187
- typeof ListPromotionsQueryDto === "undefined" ? Object : ListPromotionsQueryDto
2188
- ]),
2278
+ _ts_metadata9("design:paramtypes", []),
2189
2279
  _ts_metadata9("design:returntype", void 0)
2190
2280
  ], GeneratedPromotionsController.prototype, "list", null);
2191
2281
  _ts_decorate9([
@@ -2268,11 +2358,11 @@ var MarketingSettingsService = class {
2268
2358
  this.repo = repo;
2269
2359
  }
2270
2360
  /** Returns the stored config or `null` (in which case the full pool applies). */
2271
- get(projectKey) {
2272
- return this.repo.get(projectKey);
2361
+ get() {
2362
+ return this.repo.get();
2273
2363
  }
2274
- upsert(projectKey, data) {
2275
- return this.repo.upsert(projectKey, data);
2364
+ upsert(data) {
2365
+ return this.repo.upsert(data);
2276
2366
  }
2277
2367
  };
2278
2368
  MarketingSettingsService = _ts_decorate10([
@@ -2285,7 +2375,7 @@ MarketingSettingsService = _ts_decorate10([
2285
2375
  ], MarketingSettingsService);
2286
2376
 
2287
2377
  // src/catalog/dto/marketing-settings.dto.ts
2288
- import { ArrayUnique, IsArray as IsArray2, IsString as IsString3, Matches as Matches3 } from "class-validator";
2378
+ import { ArrayUnique, IsArray as IsArray2, IsString as IsString3, Matches as Matches2 } from "class-validator";
2289
2379
  function _ts_decorate11(decorators, target, key, desc) {
2290
2380
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2291
2381
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -2297,42 +2387,20 @@ function _ts_metadata11(k, v) {
2297
2387
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
2298
2388
  }
2299
2389
  __name(_ts_metadata11, "_ts_metadata");
2300
- var PROJECT_KEY_PATTERN3 = /^[a-z][a-z0-9-]*$/;
2301
2390
  var LOCALE_PATTERN = /^[a-z]{2}(-[A-Z]{2})?$/;
2302
- var ListMarketingSettingsQueryDto = class {
2303
- static {
2304
- __name(this, "ListMarketingSettingsQueryDto");
2305
- }
2306
- projectKey;
2307
- };
2308
- _ts_decorate11([
2309
- IsString3(),
2310
- Matches3(PROJECT_KEY_PATTERN3, {
2311
- message: "projectKey must be kebab-case"
2312
- }),
2313
- _ts_metadata11("design:type", String)
2314
- ], ListMarketingSettingsQueryDto.prototype, "projectKey", void 0);
2315
2391
  var UpdateMarketingSettingsDto = class {
2316
2392
  static {
2317
2393
  __name(this, "UpdateMarketingSettingsDto");
2318
2394
  }
2319
- projectKey;
2320
2395
  activeLocales;
2321
2396
  };
2322
- _ts_decorate11([
2323
- IsString3(),
2324
- Matches3(PROJECT_KEY_PATTERN3, {
2325
- message: "projectKey must be kebab-case"
2326
- }),
2327
- _ts_metadata11("design:type", String)
2328
- ], UpdateMarketingSettingsDto.prototype, "projectKey", void 0);
2329
2397
  _ts_decorate11([
2330
2398
  IsArray2(),
2331
2399
  ArrayUnique(),
2332
2400
  IsString3({
2333
2401
  each: true
2334
2402
  }),
2335
- Matches3(LOCALE_PATTERN, {
2403
+ Matches2(LOCALE_PATTERN, {
2336
2404
  each: true,
2337
2405
  message: "Locale must be ISO-639-1"
2338
2406
  }),
@@ -2340,7 +2408,7 @@ _ts_decorate11([
2340
2408
  ], UpdateMarketingSettingsDto.prototype, "activeLocales", void 0);
2341
2409
 
2342
2410
  // src/catalog/marketing-settings.controller.ts
2343
- import { Body as Body3, Controller as Controller3, Get as Get3, Inject as Inject9, Put, Query as Query3, UseGuards as UseGuards3 } from "@nestjs/common";
2411
+ import { Body as Body3, Controller as Controller3, Get as Get3, Inject as Inject9, Put, UseGuards as UseGuards3 } from "@nestjs/common";
2344
2412
  function _ts_decorate12(decorators, target, key, desc) {
2345
2413
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2346
2414
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -2368,22 +2436,19 @@ function buildMarketingSettingsController(guards) {
2368
2436
  this.service = service;
2369
2437
  }
2370
2438
  /** Returns the stored config or `null` (full pool active). */
2371
- get(query) {
2372
- return this.service.get(query.projectKey);
2439
+ get() {
2440
+ return this.service.get();
2373
2441
  }
2374
2442
  update(dto) {
2375
- return this.service.upsert(dto.projectKey, {
2443
+ return this.service.upsert({
2376
2444
  activeLocales: dto.activeLocales
2377
2445
  });
2378
2446
  }
2379
2447
  };
2380
2448
  _ts_decorate12([
2381
2449
  Get3(),
2382
- _ts_param9(0, Query3()),
2383
2450
  _ts_metadata12("design:type", Function),
2384
- _ts_metadata12("design:paramtypes", [
2385
- typeof ListMarketingSettingsQueryDto === "undefined" ? Object : ListMarketingSettingsQueryDto
2386
- ]),
2451
+ _ts_metadata12("design:paramtypes", []),
2387
2452
  _ts_metadata12("design:returntype", void 0)
2388
2453
  ], GeneratedMarketingSettingsController.prototype, "get", null);
2389
2454
  _ts_decorate12([
@@ -2410,7 +2475,7 @@ __name(buildMarketingSettingsController, "buildMarketingSettingsController");
2410
2475
 
2411
2476
  // src/catalog/public-marketing-catalog.service.ts
2412
2477
  import { Inject as Inject10, Injectable as Injectable7, Optional as Optional5 } from "@nestjs/common";
2413
- import { applyPromo, buildFeatureRequiresIndex, collectUnsatisfiedRequires, pickActivePromo } from "@saasicat/core";
2478
+ import { buildFeatureRequiresIndex, collectUnsatisfiedRequires, promotionOnPrice, keepOneRecommended } from "@saasicat/core";
2414
2479
  function _ts_decorate13(decorators, target, key, desc) {
2415
2480
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2416
2481
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -2444,14 +2509,30 @@ var PublicMarketingCatalogService = class {
2444
2509
  promotionRepo;
2445
2510
  catalogEntryRepo;
2446
2511
  bundleRepo;
2447
- constructor(planRepo, marketingRepo, promotionRepo, catalogEntryRepo = null, bundleRepo = null) {
2512
+ newPaymentMethodsSource;
2513
+ constructor(planRepo, marketingRepo, promotionRepo, catalogEntryRepo = null, bundleRepo = null, newPaymentMethodsSource) {
2448
2514
  this.planRepo = planRepo;
2449
2515
  this.marketingRepo = marketingRepo;
2450
2516
  this.promotionRepo = promotionRepo;
2451
2517
  this.catalogEntryRepo = catalogEntryRepo;
2452
2518
  this.bundleRepo = bundleRepo;
2519
+ this.newPaymentMethodsSource = newPaymentMethodsSource;
2453
2520
  }
2454
- async getCatalog(projectKey, locale, currency, vatRate, asOf = /* @__PURE__ */ new Date()) {
2521
+ /** What a new payment method is taken with here, for the page that offers the plans. */
2522
+ newPaymentMethods() {
2523
+ const account = this.newPaymentMethodsSource?.forNewPaymentMethods() ?? null;
2524
+ if (!account) return {
2525
+ taken: false,
2526
+ methods: []
2527
+ };
2528
+ return {
2529
+ taken: true,
2530
+ methods: [
2531
+ ...account.methods
2532
+ ]
2533
+ };
2534
+ }
2535
+ async getCatalog(locale, currency, vatRate, asOf = /* @__PURE__ */ new Date()) {
2455
2536
  const empty = {
2456
2537
  features: [],
2457
2538
  quotas: []
@@ -2460,10 +2541,10 @@ var PublicMarketingCatalogService = class {
2460
2541
  const findLatest = this.planRepo.findLatestLivePlanVersion?.bind(this.planRepo);
2461
2542
  if (!findActive && !findLatest) {
2462
2543
  return {
2463
- projectKey,
2464
2544
  locale,
2465
2545
  currency,
2466
2546
  vatRate,
2547
+ newPaymentMethods: this.newPaymentMethods(),
2467
2548
  plans: [],
2468
2549
  bundles: [],
2469
2550
  comparison: empty
@@ -2471,21 +2552,20 @@ var PublicMarketingCatalogService = class {
2471
2552
  }
2472
2553
  const resolveVersion = findActive ? (planKey) => findActive(planKey, asOf) : (planKey) => findLatest(planKey);
2473
2554
  const [plans, promotions, labelMeta, marketedBundles] = await Promise.all([
2474
- this.planRepo.list({
2475
- projectKey
2476
- }),
2477
- this.promotionRepo.list({
2478
- projectKey
2479
- }),
2480
- this.loadLabelMeta(projectKey, locale),
2481
- this.loadMarketedBundles(projectKey)
2555
+ this.planRepo.list({}),
2556
+ this.promotionRepo.list(),
2557
+ this.loadLabelMeta(locale),
2558
+ this.loadMarketedBundles()
2482
2559
  ]);
2483
2560
  const out = [];
2561
+ const inRequestedLocale = /* @__PURE__ */ new Set();
2484
2562
  for (const plan of plans) {
2485
2563
  const live = await resolveVersion(plan.planKey);
2486
2564
  if (!live) continue;
2487
- const marketing = await this.marketingRepo.findByTarget("PLAN", live.id, locale) ?? (locale === DEFAULT_LOCALE ? null : await this.marketingRepo.findByTarget("PLAN", live.id, DEFAULT_LOCALE));
2565
+ const own = await this.marketingRepo.findByTarget("PLAN", live.id, locale);
2566
+ const marketing = own ?? (locale === DEFAULT_LOCALE ? null : await this.marketingRepo.findByTarget("PLAN", live.id, DEFAULT_LOCALE));
2488
2567
  if (!marketing || marketing.visible === false) continue;
2568
+ if (own) inRequestedLocale.add(plan.planKey);
2489
2569
  const monthlyNet = toNumber(live.monthlyNet);
2490
2570
  const yearlyNet = toNumber(live.yearlyNet);
2491
2571
  out.push({
@@ -2513,6 +2593,7 @@ var PublicMarketingCatalogService = class {
2513
2593
  });
2514
2594
  }
2515
2595
  out.sort((a, b) => b.priority - a.priority);
2596
+ keepOneRecommended(out, inRequestedLocale);
2516
2597
  const requiresIndex = buildFeatureRequiresIndex([
2517
2598
  ...labelMeta.features
2518
2599
  ].map(([featureKey, meta]) => ({
@@ -2521,25 +2602,24 @@ var PublicMarketingCatalogService = class {
2521
2602
  })));
2522
2603
  const publicBundles = (await Promise.all(marketedBundles.map((bv) => this.toPublicBundle(bv, promotions, locale, asOf, requiresIndex, labelMeta)))).filter((bundle) => bundle !== null);
2523
2604
  return {
2524
- projectKey,
2525
2605
  locale,
2526
2606
  currency,
2527
2607
  vatRate,
2608
+ newPaymentMethods: this.newPaymentMethods(),
2528
2609
  plans: out,
2529
2610
  bundles: publicBundles,
2530
2611
  comparison: this.buildComparison(out, labelMeta)
2531
2612
  };
2532
2613
  }
2533
2614
  /**
2534
- * Loads all published-and-live bundle versions for a project and
2535
- * filters on `marketed === true`. Without a registered
2536
- * `BundleRepository` (apps without the SuperAdmin bundle editor) the
2537
- * list is empty — the public catalog stays plan-only.
2615
+ * Loads all published-and-live bundle versions and filters on
2616
+ * `marketed === true`. Without a registered `BundleRepository` (apps
2617
+ * without the SuperAdmin bundle editor) the list is empty — the public
2618
+ * catalog stays plan-only.
2538
2619
  */
2539
- async loadMarketedBundles(projectKey) {
2620
+ async loadMarketedBundles() {
2540
2621
  if (!this.bundleRepo) return [];
2541
2622
  const bundles = await this.bundleRepo.list({
2542
- projectKey,
2543
2623
  excludeDeleted: true
2544
2624
  });
2545
2625
  const out = [];
@@ -2607,7 +2687,7 @@ var PublicMarketingCatalogService = class {
2607
2687
  return labels;
2608
2688
  }
2609
2689
  /** Loads the translated feature/quota labels (+ sortOrder) for a locale. */
2610
- async loadLabelMeta(projectKey, locale) {
2690
+ async loadLabelMeta(locale) {
2611
2691
  const features = /* @__PURE__ */ new Map();
2612
2692
  const quotas = /* @__PURE__ */ new Map();
2613
2693
  if (!this.catalogEntryRepo) return {
@@ -2615,12 +2695,8 @@ var PublicMarketingCatalogService = class {
2615
2695
  quotas
2616
2696
  };
2617
2697
  const [featureRows, quotaRows] = await Promise.all([
2618
- this.catalogEntryRepo.listFeatures({
2619
- projectKey
2620
- }),
2621
- this.catalogEntryRepo.listQuotas({
2622
- projectKey
2623
- })
2698
+ this.catalogEntryRepo.listFeatures({}),
2699
+ this.catalogEntryRepo.listQuotas({})
2624
2700
  ]);
2625
2701
  for (const f of featureRows) {
2626
2702
  features.set(f.featureKey, {
@@ -2711,20 +2787,18 @@ var PublicMarketingCatalogService = class {
2711
2787
  };
2712
2788
  }
2713
2789
  resolvePromo(promotions, targetKey, targetType, locale, monthlyNet, yearlyNet, asOf) {
2714
- const monthlyPromo = pickActivePromo(promotions, targetKey, locale, "monthly", asOf, targetType);
2715
- const yearlyPromo = pickActivePromo(promotions, targetKey, locale, "yearly", asOf, targetType);
2716
- const promo = monthlyPromo ?? yearlyPromo;
2790
+ const monthly = promotionOnPrice(promotions, targetKey, locale, "monthly", monthlyNet, asOf, targetType);
2791
+ const yearly = promotionOnPrice(promotions, targetKey, locale, "yearly", yearlyNet, asOf, targetType);
2792
+ const promo = (monthly ?? yearly)?.promotion;
2717
2793
  if (!promo) return null;
2718
- const monthlyResult = applyPromo(monthlyPromo, monthlyNet);
2719
- const yearlyResult = applyPromo(yearlyPromo, yearlyNet);
2720
2794
  const i18n = promo.i18n?.[locale] ?? promo.i18n?.[DEFAULT_LOCALE] ?? {};
2721
2795
  return {
2722
2796
  type: promo.type,
2723
2797
  badge: i18n.badge ?? "",
2724
2798
  fineprint: i18n.fineprint ?? "",
2725
2799
  color: promo.color,
2726
- discountedMonthlyNet: monthlyResult ? monthlyResult.discounted : null,
2727
- discountedYearlyNet: yearlyResult ? yearlyResult.discounted : null
2800
+ discountedMonthlyNet: monthly ? monthly.result.discounted : null,
2801
+ discountedYearlyNet: yearly ? yearly.result.discounted : null
2728
2802
  };
2729
2803
  }
2730
2804
  };
@@ -2737,18 +2811,20 @@ PublicMarketingCatalogService = _ts_decorate13([
2737
2811
  _ts_param10(3, Inject10(CATALOG_ENTRY_REPOSITORY_TOKEN)),
2738
2812
  _ts_param10(4, Optional5()),
2739
2813
  _ts_param10(4, Inject10(BUNDLE_REPOSITORY_TOKEN)),
2814
+ _ts_param10(5, Inject10(NEW_PAYMENT_METHODS_SOURCE_TOKEN)),
2740
2815
  _ts_metadata13("design:type", Function),
2741
2816
  _ts_metadata13("design:paramtypes", [
2742
2817
  typeof PlanRepository === "undefined" ? Object : PlanRepository,
2743
2818
  typeof MarketingProjectionRepository === "undefined" ? Object : MarketingProjectionRepository,
2744
2819
  typeof PromotionRepository === "undefined" ? Object : PromotionRepository,
2745
2820
  Object,
2821
+ Object,
2746
2822
  Object
2747
2823
  ])
2748
2824
  ], PublicMarketingCatalogService);
2749
2825
 
2750
2826
  // src/catalog/public-marketing-catalog.controller.ts
2751
- import { Controller as Controller4, Get as Get4, Inject as Inject11, Query as Query4, UseGuards as UseGuards4 } from "@nestjs/common";
2827
+ import { Controller as Controller4, Get as Get4, Inject as Inject11, Query as Query2, UseGuards as UseGuards4 } from "@nestjs/common";
2752
2828
  function _ts_decorate14(decorators, target, key, desc) {
2753
2829
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2754
2830
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -2766,7 +2842,7 @@ function _ts_param11(paramIndex, decorator) {
2766
2842
  };
2767
2843
  }
2768
2844
  __name(_ts_param11, "_ts_param");
2769
- function buildPublicMarketingCatalogController(guards, projectKey, currency, vatRate) {
2845
+ function buildPublicMarketingCatalogController(guards, currency, vatRate) {
2770
2846
  let GeneratedPublicMarketingCatalogController = class GeneratedPublicMarketingCatalogController {
2771
2847
  static {
2772
2848
  __name(this, "GeneratedPublicMarketingCatalogController");
@@ -2785,14 +2861,14 @@ function buildPublicMarketingCatalogController(guards, projectKey, currency, vat
2785
2861
  getCatalog(lang, localeParam, asOfParam) {
2786
2862
  const locale = lang || localeParam || "de";
2787
2863
  const asOf = parseAsOf(asOfParam);
2788
- return this.service.getCatalog(projectKey, locale, currency, vatRate, asOf);
2864
+ return this.service.getCatalog(locale, currency, vatRate, asOf);
2789
2865
  }
2790
2866
  };
2791
2867
  _ts_decorate14([
2792
2868
  Get4(),
2793
- _ts_param11(0, Query4("lang")),
2794
- _ts_param11(1, Query4("locale")),
2795
- _ts_param11(2, Query4("asOf")),
2869
+ _ts_param11(0, Query2("lang")),
2870
+ _ts_param11(1, Query2("locale")),
2871
+ _ts_param11(2, Query2("asOf")),
2796
2872
  _ts_metadata14("design:type", Function),
2797
2873
  _ts_metadata14("design:paramtypes", [
2798
2874
  String,
@@ -2822,8 +2898,8 @@ function parseAsOf(raw) {
2822
2898
  __name(parseAsOf, "parseAsOf");
2823
2899
 
2824
2900
  // src/catalog/plans.service.ts
2825
- import { Inject as Inject12, Injectable as Injectable8, NotFoundException as NotFoundException5, Optional as Optional6, UnprocessableEntityException as UnprocessableEntityException3 } from "@nestjs/common";
2826
- import { CATALOG_ERROR_CODES as CATALOG_ERROR_CODES5 } from "@saasicat/core";
2901
+ import { Inject as Inject12, Injectable as Injectable8, NotFoundException as NotFoundException5, Optional as Optional6, UnprocessableEntityException as UnprocessableEntityException5 } from "@nestjs/common";
2902
+ import { CATALOG_ERROR_CODES as CATALOG_ERROR_CODES7 } from "@saasicat/core";
2827
2903
  function _ts_decorate15(decorators, target, key, desc) {
2828
2904
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2829
2905
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -2858,12 +2934,11 @@ var PlansService = class {
2858
2934
  * `SubscriptionRepository` is registered or it does not implement
2859
2935
  * `countActiveByPlanKey`.
2860
2936
  */
2861
- async getTenantCounts(projectKey) {
2862
- return await this.subscriptions?.countActiveByPlanKey?.(projectKey) ?? {};
2937
+ async getTenantCounts() {
2938
+ return await this.subscriptions?.countActiveByPlanKey?.() ?? {};
2863
2939
  }
2864
- listPlans(projectKey, opts = {}) {
2940
+ listPlans(opts = {}) {
2865
2941
  return this.repo.list({
2866
- projectKey,
2867
2942
  excludeDeleted: true,
2868
2943
  onlyPublished: opts.onlyPublished ?? false
2869
2944
  });
@@ -2872,7 +2947,7 @@ var PlansService = class {
2872
2947
  const plan = await this.repo.findById(planId);
2873
2948
  if (!plan) {
2874
2949
  throw new NotFoundException5({
2875
- code: CATALOG_ERROR_CODES5.PLAN_NOT_FOUND,
2950
+ code: CATALOG_ERROR_CODES7.PLAN_NOT_FOUND,
2876
2951
  message: `Plan '${planId}' not found`,
2877
2952
  params: {
2878
2953
  planId
@@ -2882,14 +2957,13 @@ var PlansService = class {
2882
2957
  return plan;
2883
2958
  }
2884
2959
  async createPlan(data) {
2885
- const existing = await this.repo.findByKey(data.projectKey, data.planKey);
2960
+ const existing = await this.repo.findByKey(data.planKey);
2886
2961
  if (existing) {
2887
- throw new UnprocessableEntityException3({
2888
- code: CATALOG_ERROR_CODES5.PLAN_ALREADY_EXISTS,
2889
- message: `Plan '${data.planKey}' already exists in project '${data.projectKey}'`,
2962
+ throw new UnprocessableEntityException5({
2963
+ code: CATALOG_ERROR_CODES7.PLAN_ALREADY_EXISTS,
2964
+ message: `Plan '${data.planKey}' already exists`,
2890
2965
  params: {
2891
- planKey: data.planKey,
2892
- projectKey: data.projectKey
2966
+ planKey: data.planKey
2893
2967
  }
2894
2968
  });
2895
2969
  }
@@ -2899,7 +2973,7 @@ var PlansService = class {
2899
2973
  const existing = await this.repo.findById(planId);
2900
2974
  if (!existing) {
2901
2975
  throw new NotFoundException5({
2902
- code: CATALOG_ERROR_CODES5.PLAN_NOT_FOUND,
2976
+ code: CATALOG_ERROR_CODES7.PLAN_NOT_FOUND,
2903
2977
  message: `Plan '${planId}' not found`,
2904
2978
  params: {
2905
2979
  planId
@@ -2912,7 +2986,7 @@ var PlansService = class {
2912
2986
  const existing = await this.repo.findById(planId);
2913
2987
  if (!existing) {
2914
2988
  throw new NotFoundException5({
2915
- code: CATALOG_ERROR_CODES5.PLAN_NOT_FOUND,
2989
+ code: CATALOG_ERROR_CODES7.PLAN_NOT_FOUND,
2916
2990
  message: `Plan '${planId}' not found`,
2917
2991
  params: {
2918
2992
  planId
@@ -2937,7 +3011,7 @@ var PlansService = class {
2937
3011
  const existing = await this.repo.findById(planId);
2938
3012
  if (!existing) {
2939
3013
  throw new NotFoundException5({
2940
- code: CATALOG_ERROR_CODES5.PLAN_NOT_FOUND,
3014
+ code: CATALOG_ERROR_CODES7.PLAN_NOT_FOUND,
2941
3015
  message: `Plan '${planId}' not found`,
2942
3016
  params: {
2943
3017
  planId
@@ -2947,8 +3021,8 @@ var PlansService = class {
2947
3021
  await this.assertNoPublishedVersions(existing.planKey, "hard-delete");
2948
3022
  await this.assertNoDrafts(existing.planKey);
2949
3023
  if (typeof this.repo.hardDelete !== "function") {
2950
- throw new UnprocessableEntityException3({
2951
- code: CATALOG_ERROR_CODES5.PLAN_HARD_DELETE_NOT_IMPLEMENTED,
3024
+ throw new UnprocessableEntityException5({
3025
+ code: CATALOG_ERROR_CODES7.PLAN_HARD_DELETE_NOT_IMPLEMENTED,
2952
3026
  message: "Hard delete is not implemented in the current repository. Implement PlanRepository.hardDelete."
2953
3027
  });
2954
3028
  }
@@ -2968,8 +3042,8 @@ var PlansService = class {
2968
3042
  if (published.length === 0) return;
2969
3043
  const live = published.filter((v) => v.supersededAt === null).length;
2970
3044
  const superseded = published.length - live;
2971
- throw new UnprocessableEntityException3({
2972
- code: CATALOG_ERROR_CODES5.PLAN_HAS_PUBLISHED_VERSIONS,
3045
+ throw new UnprocessableEntityException5({
3046
+ code: CATALOG_ERROR_CODES7.PLAN_HAS_PUBLISHED_VERSIONS,
2973
3047
  message: `Plan '${planKey}' cannot be ${op === "hard-delete" ? "deleted" : "archived"} \u2014 it has ${published.length} published version(s) (${live} live, ${superseded} superseded). Existing subscriptions reference those versions (contract protection P1), so the plan record must be preserved.`,
2974
3048
  params: {
2975
3049
  planKey,
@@ -2985,8 +3059,8 @@ var PlansService = class {
2985
3059
  const versions = await this.repo.listVersions(planKey);
2986
3060
  const drafts = versions.filter((v) => v.publishedAt === null);
2987
3061
  if (drafts.length === 0) return;
2988
- throw new UnprocessableEntityException3({
2989
- code: CATALOG_ERROR_CODES5.PLAN_HAS_DRAFTS,
3062
+ throw new UnprocessableEntityException5({
3063
+ code: CATALOG_ERROR_CODES7.PLAN_HAS_DRAFTS,
2990
3064
  message: `Plan '${planKey}' still has ${drafts.length} open draft version(s). Discard them first (DELETE /admin/catalog/plan-versions/:id) or publish them.`,
2991
3065
  params: {
2992
3066
  planKey
@@ -3008,8 +3082,8 @@ PlansService = _ts_decorate15([
3008
3082
  ], PlansService);
3009
3083
 
3010
3084
  // src/catalog/plan-versions.service.ts
3011
- import { Inject as Inject13, Injectable as Injectable9, Logger as Logger3, NotFoundException as NotFoundException6, Optional as Optional7, UnprocessableEntityException as UnprocessableEntityException4 } from "@nestjs/common";
3012
- import { CATALOG_ERROR_CODES as CATALOG_ERROR_CODES6, isVersionEditable as isVersionEditable2 } from "@saasicat/core";
3085
+ import { Inject as Inject13, Injectable as Injectable9, Logger as Logger3, NotFoundException as NotFoundException6, Optional as Optional7, UnprocessableEntityException as UnprocessableEntityException6 } from "@nestjs/common";
3086
+ import { CATALOG_ERROR_CODES as CATALOG_ERROR_CODES8, isVersionEditable as isVersionEditable2 } from "@saasicat/core";
3013
3087
  function _ts_decorate16(decorators, target, key, desc) {
3014
3088
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3015
3089
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -3079,7 +3153,7 @@ var PlanVersionsService = class _PlanVersionsService {
3079
3153
  const existing = await this.repo.findVersionById(versionId);
3080
3154
  if (!existing) {
3081
3155
  throw new NotFoundException6({
3082
- code: CATALOG_ERROR_CODES6.PLAN_VERSION_NOT_FOUND,
3156
+ code: CATALOG_ERROR_CODES8.PLAN_VERSION_NOT_FOUND,
3083
3157
  message: `PlanVersion '${versionId}' not found`,
3084
3158
  params: {
3085
3159
  versionId
@@ -3087,8 +3161,8 @@ var PlanVersionsService = class _PlanVersionsService {
3087
3161
  });
3088
3162
  }
3089
3163
  if (existing.publishedAt !== null) {
3090
- throw new UnprocessableEntityException4({
3091
- code: CATALOG_ERROR_CODES6.PLAN_VERSION_ALREADY_PUBLISHED,
3164
+ throw new UnprocessableEntityException6({
3165
+ code: CATALOG_ERROR_CODES8.PLAN_VERSION_ALREADY_PUBLISHED,
3092
3166
  message: `PlanVersion '${versionId}' is already published and cannot be discarded.`,
3093
3167
  params: {
3094
3168
  versionId
@@ -3096,8 +3170,8 @@ var PlanVersionsService = class _PlanVersionsService {
3096
3170
  });
3097
3171
  }
3098
3172
  if (typeof this.repo.deletePlanVersionDraft !== "function") {
3099
- throw new UnprocessableEntityException4({
3100
- code: CATALOG_ERROR_CODES6.PLAN_VERSION_DISCARD_NOT_IMPLEMENTED,
3173
+ throw new UnprocessableEntityException6({
3174
+ code: CATALOG_ERROR_CODES8.PLAN_VERSION_DISCARD_NOT_IMPLEMENTED,
3101
3175
  message: "Discard is not implemented in the current repository. Implement PlanRepository.deletePlanVersionDraft."
3102
3176
  });
3103
3177
  }
@@ -3112,7 +3186,7 @@ var PlanVersionsService = class _PlanVersionsService {
3112
3186
  const plan = await this.repo.findById(planUuid);
3113
3187
  if (!plan) {
3114
3188
  throw new NotFoundException6({
3115
- code: CATALOG_ERROR_CODES6.PLAN_NOT_FOUND,
3189
+ code: CATALOG_ERROR_CODES8.PLAN_NOT_FOUND,
3116
3190
  message: `Plan '${planUuid}' not found`,
3117
3191
  params: {
3118
3192
  planId: planUuid
@@ -3126,7 +3200,7 @@ var PlanVersionsService = class _PlanVersionsService {
3126
3200
  const version = await this.repo.findVersionById(versionId);
3127
3201
  if (!version) {
3128
3202
  throw new NotFoundException6({
3129
- code: CATALOG_ERROR_CODES6.PLAN_VERSION_NOT_FOUND,
3203
+ code: CATALOG_ERROR_CODES8.PLAN_VERSION_NOT_FOUND,
3130
3204
  message: `PlanVersion '${versionId}' not found`,
3131
3205
  params: {
3132
3206
  versionId
@@ -3168,9 +3242,9 @@ var PlanVersionsService = class _PlanVersionsService {
3168
3242
  */
3169
3243
  async createPlanDraft(data) {
3170
3244
  const plan = await this.repo.findById(data.planId);
3171
- if (!plan) {
3245
+ if (!plan || plan.deletedAt !== null) {
3172
3246
  throw new NotFoundException6({
3173
- code: CATALOG_ERROR_CODES6.PLAN_NOT_FOUND,
3247
+ code: CATALOG_ERROR_CODES8.PLAN_NOT_FOUND,
3174
3248
  message: `Plan '${data.planId}' not found`,
3175
3249
  params: {
3176
3250
  planId: data.planId
@@ -3180,8 +3254,8 @@ var PlanVersionsService = class _PlanVersionsService {
3180
3254
  const planKey = plan.planKey;
3181
3255
  const existingDraft = await this.repo.findCurrentDraft(planKey);
3182
3256
  if (existingDraft) {
3183
- throw new UnprocessableEntityException4({
3184
- code: CATALOG_ERROR_CODES6.PLAN_DRAFT_ALREADY_EXISTS,
3257
+ throw new UnprocessableEntityException6({
3258
+ code: CATALOG_ERROR_CODES8.PLAN_DRAFT_ALREADY_EXISTS,
3185
3259
  message: `Plan '${planKey}' already has a draft version v${existingDraft.version}; publish or discard it first`,
3186
3260
  params: {
3187
3261
  planKey,
@@ -3213,7 +3287,7 @@ var PlanVersionsService = class _PlanVersionsService {
3213
3287
  const existing = await this.repo.findVersionById(versionId);
3214
3288
  if (!existing) {
3215
3289
  throw new NotFoundException6({
3216
- code: CATALOG_ERROR_CODES6.PLAN_VERSION_NOT_FOUND,
3290
+ code: CATALOG_ERROR_CODES8.PLAN_VERSION_NOT_FOUND,
3217
3291
  message: `PlanVersion '${versionId}' not found`,
3218
3292
  params: {
3219
3293
  versionId
@@ -3225,8 +3299,8 @@ var PlanVersionsService = class _PlanVersionsService {
3225
3299
  ]);
3226
3300
  const { editable } = isVersionEditable2(annotated);
3227
3301
  if (!editable) {
3228
- throw new UnprocessableEntityException4({
3229
- code: CATALOG_ERROR_CODES6.PLAN_VERSION_NOT_EDITABLE,
3302
+ throw new UnprocessableEntityException6({
3303
+ code: CATALOG_ERROR_CODES8.PLAN_VERSION_NOT_EDITABLE,
3230
3304
  message: `PlanVersion '${versionId}' is not editable. Only drafts and published versions are editable that are latest-in-chain, bind no subscription yet, and whose validFrom lies in the future.`,
3231
3305
  params: {
3232
3306
  versionId
@@ -3249,7 +3323,7 @@ var PlanVersionsService = class _PlanVersionsService {
3249
3323
  const draft = await this.repo.findVersionById(versionId);
3250
3324
  if (!draft) {
3251
3325
  throw new NotFoundException6({
3252
- code: CATALOG_ERROR_CODES6.PLAN_VERSION_NOT_FOUND,
3326
+ code: CATALOG_ERROR_CODES8.PLAN_VERSION_NOT_FOUND,
3253
3327
  message: `PlanVersion '${versionId}' not found`,
3254
3328
  params: {
3255
3329
  versionId
@@ -3257,14 +3331,24 @@ var PlanVersionsService = class _PlanVersionsService {
3257
3331
  });
3258
3332
  }
3259
3333
  if (draft.publishedAt !== null) {
3260
- throw new UnprocessableEntityException4({
3261
- code: CATALOG_ERROR_CODES6.PLAN_VERSION_ALREADY_PUBLISHED,
3334
+ throw new UnprocessableEntityException6({
3335
+ code: CATALOG_ERROR_CODES8.PLAN_VERSION_ALREADY_PUBLISHED,
3262
3336
  message: `PlanVersion '${versionId}' is already published`,
3263
3337
  params: {
3264
3338
  versionId
3265
3339
  }
3266
3340
  });
3267
3341
  }
3342
+ const plan = await this.repo.findByKey(draft.planId);
3343
+ if (!plan || plan.deletedAt !== null) {
3344
+ throw new NotFoundException6({
3345
+ code: CATALOG_ERROR_CODES8.PLAN_NOT_FOUND,
3346
+ message: `Plan '${draft.planId}' not found`,
3347
+ params: {
3348
+ planId: draft.planId
3349
+ }
3350
+ });
3351
+ }
3268
3352
  const warnings = await this.runStrictCheck({
3269
3353
  features: draft.features,
3270
3354
  quotas: draft.quotas ?? {}
@@ -3274,8 +3358,8 @@ var PlanVersionsService = class _PlanVersionsService {
3274
3358
  const monthly = Number.parseFloat(String(draft.monthlyNet ?? "0"));
3275
3359
  const yearly = Number.parseFloat(String(draft.yearlyNet ?? "0"));
3276
3360
  if (monthly <= 0 || yearly <= 0) {
3277
- throw new UnprocessableEntityException4({
3278
- code: CATALOG_ERROR_CODES6.PLAN_VERSION_ZERO_PRICE,
3361
+ throw new UnprocessableEntityException6({
3362
+ code: CATALOG_ERROR_CODES8.PLAN_VERSION_ZERO_PRICE,
3279
3363
  message: "A plan version cannot be published with a price of 0.00 (guard against seed placeholder). For deliberately free special contracts, set allowZeroPrice.",
3280
3364
  monthlyNet: String(draft.monthlyNet),
3281
3365
  yearlyNet: String(draft.yearlyNet)
@@ -3283,78 +3367,10 @@ var PlanVersionsService = class _PlanVersionsService {
3283
3367
  }
3284
3368
  }
3285
3369
  const previous = await this.repo.findLatestLivePlanVersion(draft.planId);
3286
- const validFromInput = publishMeta.validFrom ?? draft.validFrom;
3287
- if (!validFromInput) {
3288
- throw new UnprocessableEntityException4({
3289
- code: CATALOG_ERROR_CODES6.PLAN_VERSION_VALID_FROM_REQUIRED,
3290
- message: "validFrom must be set when publishing (on the draft or the publish call).."
3291
- });
3292
- }
3293
- const validFrom = new Date(validFromInput);
3294
- if (Number.isNaN(validFrom.getTime())) {
3295
- throw new UnprocessableEntityException4({
3296
- code: CATALOG_ERROR_CODES6.PLAN_VERSION_VALID_FROM_INVALID,
3297
- message: `validFrom '${validFromInput}' is not a valid date`,
3298
- params: {
3299
- validFrom: validFromInput
3300
- }
3301
- });
3302
- }
3303
- if (previous?.validFrom) {
3304
- const prevFrom = new Date(previous.validFrom);
3305
- if (validFrom <= prevFrom) {
3306
- throw new UnprocessableEntityException4({
3307
- code: CATALOG_ERROR_CODES6.PLAN_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS,
3308
- message: `validFrom (${validFrom.toISOString()}) must be strictly after the validFrom of the previous version (${previous.validFrom}).`,
3309
- params: {
3310
- validFrom: validFrom.toISOString(),
3311
- previousValidFrom: previous.validFrom
3312
- }
3313
- });
3314
- }
3315
- if (previous.validUntil) {
3316
- const prevUntil = new Date(previous.validUntil);
3317
- const dayMs = 24 * 60 * 60 * 1e3;
3318
- const requiredStart = new Date(prevUntil.getTime() + dayMs);
3319
- if (validFrom.getTime() !== requiredStart.getTime()) {
3320
- throw new UnprocessableEntityException4({
3321
- code: CATALOG_ERROR_CODES6.PLAN_VERSION_VALID_FROM_NOT_GAPLESS,
3322
- message: `The predecessor has validUntil=${previous.validUntil.slice(0, 10)} \u2014 the successor must start seamlessly on the next day (${requiredStart.toISOString().slice(0, 10)}). Received: ${validFrom.toISOString().slice(0, 10)}.`,
3323
- params: {
3324
- received: validFrom.toISOString().slice(0, 10)
3325
- },
3326
- requiredValidFrom: requiredStart.toISOString().slice(0, 10),
3327
- previousValidUntil: previous.validUntil
3328
- });
3329
- }
3330
- }
3331
- }
3332
- const validUntilInput = publishMeta.validUntil !== void 0 ? publishMeta.validUntil : draft.validUntil;
3333
- const validUntil = validUntilInput ? new Date(validUntilInput) : null;
3334
- if (validUntil && Number.isNaN(validUntil.getTime())) {
3335
- throw new UnprocessableEntityException4({
3336
- code: CATALOG_ERROR_CODES6.PLAN_VERSION_VALID_UNTIL_INVALID,
3337
- message: `validUntil '${validUntilInput}' is not a valid date`,
3338
- params: {
3339
- validUntil: validUntilInput
3340
- }
3341
- });
3342
- }
3343
- if (validUntil && validUntil <= validFrom) {
3344
- throw new UnprocessableEntityException4({
3345
- code: CATALOG_ERROR_CODES6.PLAN_VERSION_VALID_UNTIL_BEFORE_FROM,
3346
- message: `validUntil (${validUntil.toISOString()}) must be strictly after validFrom (${validFrom.toISOString()}).`,
3347
- params: {
3348
- validUntil: validUntil.toISOString(),
3349
- validFrom: validFrom.toISOString()
3350
- }
3351
- });
3352
- }
3370
+ const { validFrom, validUntil } = resolveValidityWindow(publishMeta, draft, previous, PLAN_VERSION_WINDOW_CODES);
3353
3371
  const fieldsFor = /* @__PURE__ */ __name((v) => ({
3354
3372
  features: v.features,
3355
- maxUsers: v.quotas?.["users"] ?? 0,
3356
- maxVehicles: v.quotas?.["vehicles"] ?? 0,
3357
- maxStorageGb: v.quotas?.["storageGb"] ?? 0,
3373
+ quotas: v.quotas ?? {},
3358
3374
  monthlyNet: v.monthlyNet,
3359
3375
  yearlyNet: v.yearlyNet
3360
3376
  }), "fieldsFor");
@@ -3363,8 +3379,8 @@ var PlanVersionsService = class _PlanVersionsService {
3363
3379
  nonRegressive: true
3364
3380
  };
3365
3381
  if (!diff.nonRegressive && !publishMeta.forceRegressive) {
3366
- throw new UnprocessableEntityException4({
3367
- code: CATALOG_ERROR_CODES6.PLAN_VERSION_REGRESSION,
3382
+ throw new UnprocessableEntityException6({
3383
+ code: CATALOG_ERROR_CODES8.PLAN_VERSION_REGRESSION,
3368
3384
  message: "This plan version is regressive (feature removed / quota lowered / price raised). Publishing requires an explicit `forceRegressive: true` (UI confirmation dialog with MFA).",
3369
3385
  changes: diff.changes
3370
3386
  });
@@ -3401,7 +3417,7 @@ var PlanVersionsService = class _PlanVersionsService {
3401
3417
  const existing = await this.repo.findVersionById(versionId);
3402
3418
  if (!existing) {
3403
3419
  throw new NotFoundException6({
3404
- code: CATALOG_ERROR_CODES6.PLAN_VERSION_NOT_FOUND,
3420
+ code: CATALOG_ERROR_CODES8.PLAN_VERSION_NOT_FOUND,
3405
3421
  message: `PlanVersion '${versionId}' not found`,
3406
3422
  params: {
3407
3423
  versionId
@@ -3409,8 +3425,8 @@ var PlanVersionsService = class _PlanVersionsService {
3409
3425
  });
3410
3426
  }
3411
3427
  if (existing.publishedAt === null) {
3412
- throw new UnprocessableEntityException4({
3413
- code: CATALOG_ERROR_CODES6.PLAN_VERSION_NOT_PUBLISHED,
3428
+ throw new UnprocessableEntityException6({
3429
+ code: CATALOG_ERROR_CODES8.PLAN_VERSION_NOT_PUBLISHED,
3414
3430
  message: `PlanVersion '${versionId}' is not published and cannot be terminated.`,
3415
3431
  params: {
3416
3432
  versionId
@@ -3418,8 +3434,8 @@ var PlanVersionsService = class _PlanVersionsService {
3418
3434
  });
3419
3435
  }
3420
3436
  if (existing.supersededAt !== null) {
3421
- throw new UnprocessableEntityException4({
3422
- code: CATALOG_ERROR_CODES6.PLAN_VERSION_SUPERSEDED,
3437
+ throw new UnprocessableEntityException6({
3438
+ code: CATALOG_ERROR_CODES8.PLAN_VERSION_SUPERSEDED,
3423
3439
  message: `PlanVersion '${versionId}' has already been superseded by a newer version (supersededAt) and cannot be terminated.`,
3424
3440
  params: {
3425
3441
  versionId
@@ -3427,14 +3443,14 @@ var PlanVersionsService = class _PlanVersionsService {
3427
3443
  });
3428
3444
  }
3429
3445
  if (Number.isNaN(endsAt.getTime())) {
3430
- throw new UnprocessableEntityException4({
3431
- code: CATALOG_ERROR_CODES6.PLAN_TERMINATE_INVALID_DATE,
3446
+ throw new UnprocessableEntityException6({
3447
+ code: CATALOG_ERROR_CODES8.PLAN_TERMINATE_INVALID_DATE,
3432
3448
  message: "endsAt is not a valid date."
3433
3449
  });
3434
3450
  }
3435
3451
  if (endsAt.getTime() <= Date.now()) {
3436
- throw new UnprocessableEntityException4({
3437
- code: CATALOG_ERROR_CODES6.PLAN_TERMINATE_DATE_NOT_FUTURE,
3452
+ throw new UnprocessableEntityException6({
3453
+ code: CATALOG_ERROR_CODES8.PLAN_TERMINATE_DATE_NOT_FUTURE,
3438
3454
  message: `endsAt (${endsAt.toISOString()}) must lie strictly in the future.`,
3439
3455
  params: {
3440
3456
  endsAt: endsAt.toISOString()
@@ -3442,8 +3458,8 @@ var PlanVersionsService = class _PlanVersionsService {
3442
3458
  });
3443
3459
  }
3444
3460
  if (typeof this.repo.terminate !== "function") {
3445
- throw new UnprocessableEntityException4({
3446
- code: CATALOG_ERROR_CODES6.PLAN_TERMINATE_NOT_IMPLEMENTED,
3461
+ throw new UnprocessableEntityException6({
3462
+ code: CATALOG_ERROR_CODES8.PLAN_TERMINATE_NOT_IMPLEMENTED,
3447
3463
  message: "Terminate is not implemented in the current repository. Implement PlanRepository.terminate."
3448
3464
  });
3449
3465
  }
@@ -3455,14 +3471,14 @@ var PlanVersionsService = class _PlanVersionsService {
3455
3471
  async runStrictCheck(draft) {
3456
3472
  const snapshot = resolveDiscoverySnapshot(this.snapshot, this.scanner);
3457
3473
  if (!snapshot) return [];
3458
- const approved = await loadApprovedCatalogKeys(this.catalogEntries, snapshot.app.key);
3474
+ const approved = await loadApprovedCatalogKeys(this.catalogEntries);
3459
3475
  return validatePlanDraft(draft, snapshot, this.marketedOnly, approved);
3460
3476
  }
3461
3477
  gateOrPass(warnings) {
3462
3478
  if (blockingStrictModeWarnings(warnings).length === 0) return;
3463
3479
  if (this.mode === "blocking") {
3464
- throw new UnprocessableEntityException4({
3465
- code: CATALOG_ERROR_CODES6.STRICT_MODE_VIOLATIONS,
3480
+ throw new UnprocessableEntityException6({
3481
+ code: CATALOG_ERROR_CODES8.STRICT_MODE_VIOLATIONS,
3466
3482
  message: "The strict-mode check found drift against the discovery snapshot.",
3467
3483
  warnings
3468
3484
  });
@@ -3494,7 +3510,49 @@ PlanVersionsService = _ts_decorate16([
3494
3510
  ], PlanVersionsService);
3495
3511
 
3496
3512
  // src/catalog/dto/bundles.dto.ts
3497
- import { IsArray as IsArray3, IsBoolean as IsBoolean2, IsInt as IsInt2, IsNumberString, IsObject as IsObject3, IsOptional as IsOptional3, IsString as IsString4, IsUUID, Matches as Matches4, Max, MaxLength as MaxLength3, Min, MinLength, ValidateIf } from "class-validator";
3513
+ import { Type } from "class-transformer";
3514
+ import { IsArray as IsArray3, IsBoolean as IsBoolean2, IsInt as IsInt2, IsNumberString, IsObject as IsObject3, IsOptional as IsOptional3, IsString as IsString4, IsUUID, Matches as Matches3, Max, MaxLength as MaxLength3, Min, MinLength, ValidateIf, ValidateNested } from "class-validator";
3515
+
3516
+ // src/catalog/dto/quota-record.validator.ts
3517
+ import { registerDecorator } from "class-validator";
3518
+ function isQuotaValue(value) {
3519
+ return typeof value === "number" && Number.isInteger(value) && value >= -1;
3520
+ }
3521
+ __name(isQuotaValue, "isQuotaValue");
3522
+ function offendingQuotaKeys(value) {
3523
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return [];
3524
+ return Object.entries(value).filter(([, quota]) => !isQuotaValue(quota)).map(([key]) => key).sort();
3525
+ }
3526
+ __name(offendingQuotaKeys, "offendingQuotaKeys");
3527
+ function IsQuotaRecord(options) {
3528
+ return /* @__PURE__ */ __name(function register(target, propertyName) {
3529
+ registerDecorator({
3530
+ name: "isQuotaRecord",
3531
+ target: target.constructor,
3532
+ propertyName,
3533
+ options,
3534
+ validator: {
3535
+ validate(value) {
3536
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
3537
+ return false;
3538
+ }
3539
+ return offendingQuotaKeys(value).length === 0;
3540
+ },
3541
+ defaultMessage(args) {
3542
+ const bad = offendingQuotaKeys(args.value);
3543
+ if (bad.length === 0) {
3544
+ return `${args.property} must be an object of quota keys to integers`;
3545
+ }
3546
+ return `${args.property} must map every key to an integer of at least -1 (-1 = unlimited); not a number: ${bad.join(", ")}`;
3547
+ }
3548
+ }
3549
+ });
3550
+ }, "register");
3551
+ }
3552
+ __name(IsQuotaRecord, "IsQuotaRecord");
3553
+
3554
+ // src/catalog/dto/bundles.dto.ts
3555
+ import { applyDecorators } from "@nestjs/common";
3498
3556
  function _ts_decorate17(decorators, target, key, desc) {
3499
3557
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3500
3558
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -3506,16 +3564,30 @@ function _ts_metadata17(k, v) {
3506
3564
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
3507
3565
  }
3508
3566
  __name(_ts_metadata17, "_ts_metadata");
3567
+ var IsBoundedText = /* @__PURE__ */ __name((max) => applyDecorators(IsString4(), MaxLength3(max)), "IsBoundedText");
3568
+ var IsFeatureKeyList = /* @__PURE__ */ __name(() => applyDecorators(IsArray3(), IsString4({
3569
+ each: true
3570
+ }), Matches3(FEATURE_KEY_PATTERN, {
3571
+ each: true,
3572
+ message: "features entries must be SCREAMING_SNAKE_CASE"
3573
+ })), "IsFeatureKeyList");
3574
+ var IsDecimalAmountOrNull = /* @__PURE__ */ __name((field) => applyDecorators(ValidateIf((_o, value) => value !== null), IsNumberString({
3575
+ no_symbols: false
3576
+ }), Matches3(DECIMAL_PATTERN, {
3577
+ message: `${field} must be a decimal with at most 2 fraction digits (e.g. "9.90")`
3578
+ })), "IsDecimalAmountOrNull");
3579
+ var IsIsoDateOrNull = /* @__PURE__ */ __name((field) => applyDecorators(ValidateIf((_o, value) => value !== null), Matches3(ISO_DATE_PATTERN2, {
3580
+ message: `${field} must be an ISO date (YYYY-MM-DD)`
3581
+ })), "IsIsoDateOrNull");
3582
+ var IsSortOrder = /* @__PURE__ */ __name(() => applyDecorators(IsInt2(), Min(0), Max(1e4)), "IsSortOrder");
3509
3583
  var KEY_PATTERN = /^[A-Z][A-Z0-9_]*$/;
3510
3584
  var FEATURE_KEY_PATTERN = /^[A-Z][A-Z0-9_]*$/;
3511
- var PROJECT_KEY_PATTERN4 = /^[a-z][a-z0-9-]*$/;
3512
3585
  var DECIMAL_PATTERN = /^\d+(\.\d{1,2})?$/;
3513
3586
  var ISO_DATE_PATTERN2 = /^\d{4}-\d{2}-\d{2}(T.*)?$/;
3514
3587
  var CreateBundleDto = class {
3515
3588
  static {
3516
3589
  __name(this, "CreateBundleDto");
3517
3590
  }
3518
- projectKey;
3519
3591
  bundleKey;
3520
3592
  label;
3521
3593
  description;
@@ -3526,15 +3598,7 @@ var CreateBundleDto = class {
3526
3598
  };
3527
3599
  _ts_decorate17([
3528
3600
  IsString4(),
3529
- Matches4(PROJECT_KEY_PATTERN4, {
3530
- message: 'projectKey must be kebab-case (e.g. "my-app")'
3531
- }),
3532
- MaxLength3(64),
3533
- _ts_metadata17("design:type", String)
3534
- ], CreateBundleDto.prototype, "projectKey", void 0);
3535
- _ts_decorate17([
3536
- IsString4(),
3537
- Matches4(KEY_PATTERN, {
3601
+ Matches3(KEY_PATTERN, {
3538
3602
  message: 'bundleKey must be SCREAMING_SNAKE_CASE (e.g. "BANKING")'
3539
3603
  }),
3540
3604
  MaxLength3(64),
@@ -3548,21 +3612,17 @@ _ts_decorate17([
3548
3612
  ], CreateBundleDto.prototype, "label", void 0);
3549
3613
  _ts_decorate17([
3550
3614
  IsOptional3(),
3551
- IsString4(),
3552
- MaxLength3(2e3),
3615
+ IsBoundedText(2e3),
3553
3616
  _ts_metadata17("design:type", String)
3554
3617
  ], CreateBundleDto.prototype, "description", void 0);
3555
3618
  _ts_decorate17([
3556
3619
  IsOptional3(),
3557
- IsString4(),
3558
- MaxLength3(64),
3620
+ IsBoundedText(64),
3559
3621
  _ts_metadata17("design:type", String)
3560
3622
  ], CreateBundleDto.prototype, "icon", void 0);
3561
3623
  _ts_decorate17([
3562
3624
  IsOptional3(),
3563
- IsInt2(),
3564
- Min(0),
3565
- Max(1e4),
3625
+ IsSortOrder(),
3566
3626
  _ts_metadata17("design:type", Number)
3567
3627
  ], CreateBundleDto.prototype, "sortOrder", void 0);
3568
3628
  _ts_decorate17([
@@ -3590,21 +3650,17 @@ _ts_decorate17([
3590
3650
  ], UpdateBundleDto.prototype, "label", void 0);
3591
3651
  _ts_decorate17([
3592
3652
  IsOptional3(),
3593
- IsString4(),
3594
- MaxLength3(2e3),
3653
+ IsBoundedText(2e3),
3595
3654
  _ts_metadata17("design:type", Object)
3596
3655
  ], UpdateBundleDto.prototype, "description", void 0);
3597
3656
  _ts_decorate17([
3598
3657
  IsOptional3(),
3599
- IsString4(),
3600
- MaxLength3(64),
3658
+ IsBoundedText(64),
3601
3659
  _ts_metadata17("design:type", Object)
3602
3660
  ], UpdateBundleDto.prototype, "icon", void 0);
3603
3661
  _ts_decorate17([
3604
3662
  IsOptional3(),
3605
- IsInt2(),
3606
- Min(0),
3607
- Max(1e4),
3663
+ IsSortOrder(),
3608
3664
  _ts_metadata17("design:type", Number)
3609
3665
  ], UpdateBundleDto.prototype, "sortOrder", void 0);
3610
3666
  _ts_decorate17([
@@ -3612,107 +3668,33 @@ _ts_decorate17([
3612
3668
  IsObject3(),
3613
3669
  _ts_metadata17("design:type", typeof Record === "undefined" ? Object : Record)
3614
3670
  ], UpdateBundleDto.prototype, "i18n", void 0);
3615
- var CreateBundleVersionDraftDto = class {
3671
+ var BundlePricingOverrideDto = class {
3616
3672
  static {
3617
- __name(this, "CreateBundleVersionDraftDto");
3673
+ __name(this, "BundlePricingOverrideDto");
3618
3674
  }
3619
- features;
3620
- quotas;
3621
- compatibility;
3622
- pricingOverrides;
3675
+ planId;
3623
3676
  monthlyNet;
3624
3677
  yearlyNet;
3625
- marketed;
3626
- changeNote;
3627
- baseVersionId;
3628
- validFrom;
3629
- validUntil;
3630
3678
  };
3631
- _ts_decorate17([
3632
- IsArray3(),
3633
- IsString4({
3634
- each: true
3635
- }),
3636
- Matches4(FEATURE_KEY_PATTERN, {
3637
- each: true,
3638
- message: "features entries must be SCREAMING_SNAKE_CASE"
3639
- }),
3640
- _ts_metadata17("design:type", Array)
3641
- ], CreateBundleVersionDraftDto.prototype, "features", void 0);
3642
- _ts_decorate17([
3643
- IsOptional3(),
3644
- IsObject3(),
3645
- _ts_metadata17("design:type", typeof Record === "undefined" ? Object : Record)
3646
- ], CreateBundleVersionDraftDto.prototype, "quotas", void 0);
3647
- _ts_decorate17([
3648
- IsOptional3(),
3649
- IsObject3(),
3650
- _ts_metadata17("design:type", Object)
3651
- ], CreateBundleVersionDraftDto.prototype, "compatibility", void 0);
3652
- _ts_decorate17([
3653
- IsOptional3(),
3654
- IsArray3(),
3655
- _ts_metadata17("design:type", typeof Array === "undefined" ? Object : Array)
3656
- ], CreateBundleVersionDraftDto.prototype, "pricingOverrides", void 0);
3657
- _ts_decorate17([
3658
- IsOptional3(),
3659
- ValidateIf((_o, value) => value !== null),
3660
- IsNumberString({
3661
- no_symbols: false
3662
- }),
3663
- Matches4(DECIMAL_PATTERN, {
3664
- message: 'monthlyNet must be a decimal with at most 2 fraction digits (e.g. "9.90")'
3665
- }),
3666
- _ts_metadata17("design:type", Object)
3667
- ], CreateBundleVersionDraftDto.prototype, "monthlyNet", void 0);
3668
- _ts_decorate17([
3669
- IsOptional3(),
3670
- ValidateIf((_o, value) => value !== null),
3671
- IsNumberString({
3672
- no_symbols: false
3673
- }),
3674
- Matches4(DECIMAL_PATTERN, {
3675
- message: "yearlyNet must be a decimal with at most 2 fraction digits"
3676
- }),
3677
- _ts_metadata17("design:type", Object)
3678
- ], CreateBundleVersionDraftDto.prototype, "yearlyNet", void 0);
3679
- _ts_decorate17([
3680
- IsOptional3(),
3681
- IsBoolean2(),
3682
- _ts_metadata17("design:type", Boolean)
3683
- ], CreateBundleVersionDraftDto.prototype, "marketed", void 0);
3684
3679
  _ts_decorate17([
3685
3680
  IsOptional3(),
3686
3681
  IsString4(),
3687
- MaxLength3(2e3),
3688
3682
  _ts_metadata17("design:type", String)
3689
- ], CreateBundleVersionDraftDto.prototype, "changeNote", void 0);
3690
- _ts_decorate17([
3691
- IsOptional3(),
3692
- IsUUID(),
3693
- _ts_metadata17("design:type", Object)
3694
- ], CreateBundleVersionDraftDto.prototype, "baseVersionId", void 0);
3683
+ ], BundlePricingOverrideDto.prototype, "planId", void 0);
3695
3684
  _ts_decorate17([
3696
3685
  IsOptional3(),
3697
- ValidateIf((_o, value) => value !== null),
3698
- Matches4(ISO_DATE_PATTERN2, {
3699
- message: "validFrom must be an ISO date (YYYY-MM-DD)"
3700
- }),
3686
+ IsDecimalAmountOrNull("pricingOverrides.monthlyNet"),
3701
3687
  _ts_metadata17("design:type", Object)
3702
- ], CreateBundleVersionDraftDto.prototype, "validFrom", void 0);
3688
+ ], BundlePricingOverrideDto.prototype, "monthlyNet", void 0);
3703
3689
  _ts_decorate17([
3704
3690
  IsOptional3(),
3705
- ValidateIf((_o, value) => value !== null),
3706
- Matches4(ISO_DATE_PATTERN2, {
3707
- message: "validUntil must be an ISO date (YYYY-MM-DD)"
3708
- }),
3691
+ IsDecimalAmountOrNull("pricingOverrides.yearlyNet"),
3709
3692
  _ts_metadata17("design:type", Object)
3710
- ], CreateBundleVersionDraftDto.prototype, "validUntil", void 0);
3711
- var UpdateBundleVersionDraftDto = class {
3693
+ ], BundlePricingOverrideDto.prototype, "yearlyNet", void 0);
3694
+ var BundleVersionDraftFieldsDto = class {
3712
3695
  static {
3713
- __name(this, "UpdateBundleVersionDraftDto");
3696
+ __name(this, "BundleVersionDraftFieldsDto");
3714
3697
  }
3715
- features;
3716
3698
  quotas;
3717
3699
  compatibility;
3718
3700
  pricingOverrides;
@@ -3720,74 +3702,86 @@ var UpdateBundleVersionDraftDto = class {
3720
3702
  yearlyNet;
3721
3703
  marketed;
3722
3704
  changeNote;
3705
+ baseVersionId;
3723
3706
  validFrom;
3724
3707
  validUntil;
3725
3708
  };
3726
- _ts_decorate17([
3727
- IsOptional3(),
3728
- IsArray3(),
3729
- IsString4({
3730
- each: true
3731
- }),
3732
- Matches4(FEATURE_KEY_PATTERN, {
3733
- each: true
3734
- }),
3735
- _ts_metadata17("design:type", Array)
3736
- ], UpdateBundleVersionDraftDto.prototype, "features", void 0);
3737
3709
  _ts_decorate17([
3738
3710
  IsOptional3(),
3739
3711
  IsObject3(),
3712
+ IsQuotaRecord(),
3740
3713
  _ts_metadata17("design:type", typeof Record === "undefined" ? Object : Record)
3741
- ], UpdateBundleVersionDraftDto.prototype, "quotas", void 0);
3714
+ ], BundleVersionDraftFieldsDto.prototype, "quotas", void 0);
3742
3715
  _ts_decorate17([
3743
3716
  IsOptional3(),
3744
3717
  IsObject3(),
3745
3718
  _ts_metadata17("design:type", Object)
3746
- ], UpdateBundleVersionDraftDto.prototype, "compatibility", void 0);
3719
+ ], BundleVersionDraftFieldsDto.prototype, "compatibility", void 0);
3747
3720
  _ts_decorate17([
3748
3721
  IsOptional3(),
3749
3722
  IsArray3(),
3750
- _ts_metadata17("design:type", typeof Array === "undefined" ? Object : Array)
3751
- ], UpdateBundleVersionDraftDto.prototype, "pricingOverrides", void 0);
3723
+ ValidateNested({
3724
+ each: true
3725
+ }),
3726
+ Type(() => BundlePricingOverrideDto),
3727
+ _ts_metadata17("design:type", Array)
3728
+ ], BundleVersionDraftFieldsDto.prototype, "pricingOverrides", void 0);
3752
3729
  _ts_decorate17([
3753
3730
  IsOptional3(),
3754
- ValidateIf((_o, value) => value !== null),
3755
- Matches4(DECIMAL_PATTERN),
3731
+ IsDecimalAmountOrNull("monthlyNet"),
3756
3732
  _ts_metadata17("design:type", Object)
3757
- ], UpdateBundleVersionDraftDto.prototype, "monthlyNet", void 0);
3733
+ ], BundleVersionDraftFieldsDto.prototype, "monthlyNet", void 0);
3758
3734
  _ts_decorate17([
3759
3735
  IsOptional3(),
3760
- ValidateIf((_o, value) => value !== null),
3761
- Matches4(DECIMAL_PATTERN),
3736
+ IsDecimalAmountOrNull("yearlyNet"),
3762
3737
  _ts_metadata17("design:type", Object)
3763
- ], UpdateBundleVersionDraftDto.prototype, "yearlyNet", void 0);
3738
+ ], BundleVersionDraftFieldsDto.prototype, "yearlyNet", void 0);
3764
3739
  _ts_decorate17([
3765
3740
  IsOptional3(),
3766
3741
  IsBoolean2(),
3767
3742
  _ts_metadata17("design:type", Boolean)
3768
- ], UpdateBundleVersionDraftDto.prototype, "marketed", void 0);
3743
+ ], BundleVersionDraftFieldsDto.prototype, "marketed", void 0);
3769
3744
  _ts_decorate17([
3770
3745
  IsOptional3(),
3771
- IsString4(),
3772
- MaxLength3(2e3),
3746
+ IsBoundedText(2e3),
3773
3747
  _ts_metadata17("design:type", String)
3774
- ], UpdateBundleVersionDraftDto.prototype, "changeNote", void 0);
3748
+ ], BundleVersionDraftFieldsDto.prototype, "changeNote", void 0);
3775
3749
  _ts_decorate17([
3776
3750
  IsOptional3(),
3777
- ValidateIf((_o, value) => value !== null),
3778
- Matches4(ISO_DATE_PATTERN2, {
3779
- message: "validFrom must be an ISO date (YYYY-MM-DD)"
3780
- }),
3751
+ IsUUID(),
3781
3752
  _ts_metadata17("design:type", Object)
3782
- ], UpdateBundleVersionDraftDto.prototype, "validFrom", void 0);
3753
+ ], BundleVersionDraftFieldsDto.prototype, "baseVersionId", void 0);
3783
3754
  _ts_decorate17([
3784
3755
  IsOptional3(),
3785
- ValidateIf((_o, value) => value !== null),
3786
- Matches4(ISO_DATE_PATTERN2, {
3787
- message: "validUntil must be an ISO date (YYYY-MM-DD)"
3788
- }),
3756
+ IsIsoDateOrNull("validFrom"),
3757
+ _ts_metadata17("design:type", Object)
3758
+ ], BundleVersionDraftFieldsDto.prototype, "validFrom", void 0);
3759
+ _ts_decorate17([
3760
+ IsOptional3(),
3761
+ IsIsoDateOrNull("validUntil"),
3789
3762
  _ts_metadata17("design:type", Object)
3790
- ], UpdateBundleVersionDraftDto.prototype, "validUntil", void 0);
3763
+ ], BundleVersionDraftFieldsDto.prototype, "validUntil", void 0);
3764
+ var CreateBundleVersionDraftDto = class extends BundleVersionDraftFieldsDto {
3765
+ static {
3766
+ __name(this, "CreateBundleVersionDraftDto");
3767
+ }
3768
+ features;
3769
+ };
3770
+ _ts_decorate17([
3771
+ IsFeatureKeyList(),
3772
+ _ts_metadata17("design:type", Array)
3773
+ ], CreateBundleVersionDraftDto.prototype, "features", void 0);
3774
+ var UpdateBundleVersionDraftDto = class extends BundleVersionDraftFieldsDto {
3775
+ static {
3776
+ __name(this, "UpdateBundleVersionDraftDto");
3777
+ }
3778
+ features;
3779
+ };
3780
+ _ts_decorate17([
3781
+ IsOptional3(),
3782
+ IsFeatureKeyList(),
3783
+ _ts_metadata17("design:type", Array)
3784
+ ], UpdateBundleVersionDraftDto.prototype, "features", void 0);
3791
3785
  var PublishBundleVersionDto = class {
3792
3786
  static {
3793
3787
  __name(this, "PublishBundleVersionDto");
@@ -3819,23 +3813,17 @@ _ts_decorate17([
3819
3813
  ], PublishBundleVersionDto.prototype, "allowZeroPrice", void 0);
3820
3814
  _ts_decorate17([
3821
3815
  IsOptional3(),
3822
- ValidateIf((_o, value) => value !== null),
3823
- Matches4(ISO_DATE_PATTERN2, {
3824
- message: "validFrom must be an ISO date (YYYY-MM-DD)"
3825
- }),
3816
+ IsIsoDateOrNull("validFrom"),
3826
3817
  _ts_metadata17("design:type", Object)
3827
3818
  ], PublishBundleVersionDto.prototype, "validFrom", void 0);
3828
3819
  _ts_decorate17([
3829
3820
  IsOptional3(),
3830
- ValidateIf((_o, value) => value !== null),
3831
- Matches4(ISO_DATE_PATTERN2, {
3832
- message: "validUntil must be an ISO date (YYYY-MM-DD)"
3833
- }),
3821
+ IsIsoDateOrNull("validUntil"),
3834
3822
  _ts_metadata17("design:type", Object)
3835
3823
  ], PublishBundleVersionDto.prototype, "validUntil", void 0);
3836
3824
 
3837
3825
  // src/catalog/bundles.controller.ts
3838
- import { Body as Body4, Controller as Controller5, Delete as Delete2, Get as Get5, HttpCode as HttpCode2, HttpStatus as HttpStatus2, Inject as Inject14, Param as Param3, ParseUUIDPipe as ParseUUIDPipe2, Patch as Patch3, Post as Post3, Query as Query5, UseGuards as UseGuards5 } from "@nestjs/common";
3826
+ import { Body as Body4, Controller as Controller5, Delete as Delete2, Get as Get5, HttpCode as HttpCode2, HttpStatus as HttpStatus2, Inject as Inject14, Param as Param3, ParseUUIDPipe as ParseUUIDPipe2, Patch as Patch3, Post as Post3, UseGuards as UseGuards5 } from "@nestjs/common";
3839
3827
  function _ts_decorate18(decorators, target, key, desc) {
3840
3828
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3841
3829
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -3863,8 +3851,8 @@ function buildBundlesController(guards) {
3863
3851
  this.service = service;
3864
3852
  }
3865
3853
  // ─── Root operations ───
3866
- listBundles(projectKey) {
3867
- return this.service.listBundles(projectKey);
3854
+ listBundles() {
3855
+ return this.service.listBundles();
3868
3856
  }
3869
3857
  getBundle(bundleId) {
3870
3858
  return this.service.getBundle(bundleId);
@@ -3901,11 +3889,8 @@ function buildBundlesController(guards) {
3901
3889
  };
3902
3890
  _ts_decorate18([
3903
3891
  Get5(),
3904
- _ts_param14(0, Query5("projectKey")),
3905
3892
  _ts_metadata18("design:type", Function),
3906
- _ts_metadata18("design:paramtypes", [
3907
- String
3908
- ]),
3893
+ _ts_metadata18("design:paramtypes", []),
3909
3894
  _ts_metadata18("design:returntype", void 0)
3910
3895
  ], GeneratedBundlesController.prototype, "listBundles", null);
3911
3896
  _ts_decorate18([
@@ -4039,6 +4024,7 @@ function buildBundleVersionsController(guards) {
4039
4024
  ], GeneratedBundleVersionsController.prototype, "discardDraft", null);
4040
4025
  _ts_decorate18([
4041
4026
  Post3(":id/publish"),
4027
+ EnforceMfa(),
4042
4028
  _ts_param14(0, Param3("id", new ParseUUIDPipe2())),
4043
4029
  _ts_param14(1, Body4()),
4044
4030
  _ts_metadata18("design:type", Function),
@@ -4062,8 +4048,9 @@ function buildBundleVersionsController(guards) {
4062
4048
  __name(buildBundleVersionsController, "buildBundleVersionsController");
4063
4049
 
4064
4050
  // src/catalog/dto/marketing-projections.dto.ts
4065
- import { IsArray as IsArray4, IsBoolean as IsBoolean3, IsIn as IsIn3, IsInt as IsInt3, IsOptional as IsOptional4, IsString as IsString5, IsUUID as IsUUID2, Matches as Matches5, Max as Max2, MaxLength as MaxLength4, Min as Min2, MinLength as MinLength2, ValidateNested } from "class-validator";
4066
- import { Type } from "class-transformer";
4051
+ import { IsArray as IsArray4, IsBoolean as IsBoolean3, IsIn as IsIn3, IsInt as IsInt3, IsOptional as IsOptional4, IsString as IsString5, IsUUID as IsUUID2, Matches as Matches4, Max as Max2, MaxLength as MaxLength4, Min as Min2, MinLength as MinLength2, ValidateNested as ValidateNested2 } from "class-validator";
4052
+ import { Type as Type2 } from "class-transformer";
4053
+ import { MARKETING_PRIORITY_MAX, MARKETING_PRIORITY_MIN } from "@saasicat/core";
4067
4054
  function _ts_decorate19(decorators, target, key, desc) {
4068
4055
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4069
4056
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -4075,7 +4062,6 @@ function _ts_metadata19(k, v) {
4075
4062
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
4076
4063
  }
4077
4064
  __name(_ts_metadata19, "_ts_metadata");
4078
- var PROJECT_KEY_PATTERN5 = /^[a-z][a-z0-9-]*$/;
4079
4065
  var LOCALE_PATTERN2 = /^[a-z]{2}(-[A-Z]{2})?$/;
4080
4066
  var TARGET_TYPES2 = [
4081
4067
  "PLAN",
@@ -4112,7 +4098,6 @@ var CreateMarketingProjectionDto = class {
4112
4098
  static {
4113
4099
  __name(this, "CreateMarketingProjectionDto");
4114
4100
  }
4115
- projectKey;
4116
4101
  targetType;
4117
4102
  targetVersionId;
4118
4103
  locale;
@@ -4128,14 +4113,6 @@ var CreateMarketingProjectionDto = class {
4128
4113
  priority;
4129
4114
  highlight;
4130
4115
  };
4131
- _ts_decorate19([
4132
- IsString5(),
4133
- Matches5(PROJECT_KEY_PATTERN5, {
4134
- message: "projectKey must be kebab-case"
4135
- }),
4136
- MaxLength4(64),
4137
- _ts_metadata19("design:type", String)
4138
- ], CreateMarketingProjectionDto.prototype, "projectKey", void 0);
4139
4116
  _ts_decorate19([
4140
4117
  IsString5(),
4141
4118
  IsIn3(TARGET_TYPES2),
@@ -4148,7 +4125,7 @@ _ts_decorate19([
4148
4125
  _ts_decorate19([
4149
4126
  IsOptional4(),
4150
4127
  IsString5(),
4151
- Matches5(LOCALE_PATTERN2, {
4128
+ Matches4(LOCALE_PATTERN2, {
4152
4129
  message: 'locale must be ISO-639-1 (e.g. "de", "en", "de-AT")'
4153
4130
  }),
4154
4131
  _ts_metadata19("design:type", String)
@@ -4179,10 +4156,10 @@ _ts_decorate19([
4179
4156
  _ts_decorate19([
4180
4157
  IsOptional4(),
4181
4158
  IsArray4(),
4182
- ValidateNested({
4159
+ ValidateNested2({
4183
4160
  each: true
4184
4161
  }),
4185
- Type(() => MarketingTopFeatureDto),
4162
+ Type2(() => MarketingTopFeatureDto),
4186
4163
  _ts_metadata19("design:type", Array)
4187
4164
  ], CreateMarketingProjectionDto.prototype, "topFeatures", void 0);
4188
4165
  _ts_decorate19([
@@ -4212,8 +4189,8 @@ _ts_decorate19([
4212
4189
  _ts_decorate19([
4213
4190
  IsOptional4(),
4214
4191
  IsInt3(),
4215
- Min2(0),
4216
- Max2(1e4),
4192
+ Min2(MARKETING_PRIORITY_MIN),
4193
+ Max2(MARKETING_PRIORITY_MAX),
4217
4194
  _ts_metadata19("design:type", Number)
4218
4195
  ], CreateMarketingProjectionDto.prototype, "priority", void 0);
4219
4196
  _ts_decorate19([
@@ -4265,10 +4242,10 @@ _ts_decorate19([
4265
4242
  _ts_decorate19([
4266
4243
  IsOptional4(),
4267
4244
  IsArray4(),
4268
- ValidateNested({
4245
+ ValidateNested2({
4269
4246
  each: true
4270
4247
  }),
4271
- Type(() => MarketingTopFeatureDto),
4248
+ Type2(() => MarketingTopFeatureDto),
4272
4249
  _ts_metadata19("design:type", Array)
4273
4250
  ], UpdateMarketingProjectionDto.prototype, "topFeatures", void 0);
4274
4251
  _ts_decorate19([
@@ -4298,8 +4275,8 @@ _ts_decorate19([
4298
4275
  _ts_decorate19([
4299
4276
  IsOptional4(),
4300
4277
  IsInt3(),
4301
- Min2(0),
4302
- Max2(1e4),
4278
+ Min2(MARKETING_PRIORITY_MIN),
4279
+ Max2(MARKETING_PRIORITY_MAX),
4303
4280
  _ts_metadata19("design:type", Number)
4304
4281
  ], UpdateMarketingProjectionDto.prototype, "priority", void 0);
4305
4282
  _ts_decorate19([
@@ -4311,16 +4288,10 @@ var ListMarketingProjectionsQueryDto = class {
4311
4288
  static {
4312
4289
  __name(this, "ListMarketingProjectionsQueryDto");
4313
4290
  }
4314
- projectKey;
4315
4291
  targetType;
4316
4292
  targetVersionId;
4317
4293
  locale;
4318
4294
  };
4319
- _ts_decorate19([
4320
- IsString5(),
4321
- Matches5(PROJECT_KEY_PATTERN5),
4322
- _ts_metadata19("design:type", String)
4323
- ], ListMarketingProjectionsQueryDto.prototype, "projectKey", void 0);
4324
4295
  _ts_decorate19([
4325
4296
  IsOptional4(),
4326
4297
  IsString5(),
@@ -4335,12 +4306,12 @@ _ts_decorate19([
4335
4306
  _ts_decorate19([
4336
4307
  IsOptional4(),
4337
4308
  IsString5(),
4338
- Matches5(LOCALE_PATTERN2),
4309
+ Matches4(LOCALE_PATTERN2),
4339
4310
  _ts_metadata19("design:type", String)
4340
4311
  ], ListMarketingProjectionsQueryDto.prototype, "locale", void 0);
4341
4312
 
4342
4313
  // src/catalog/marketing-projections.controller.ts
4343
- import { Body as Body5, Controller as Controller6, Delete as Delete3, Get as Get6, HttpCode as HttpCode3, HttpStatus as HttpStatus3, Inject as Inject15, Param as Param4, ParseUUIDPipe as ParseUUIDPipe3, Patch as Patch4, Post as Post4, Query as Query6, UseGuards as UseGuards6 } from "@nestjs/common";
4314
+ import { Body as Body5, Controller as Controller6, Delete as Delete3, Get as Get6, HttpCode as HttpCode3, HttpStatus as HttpStatus3, Inject as Inject15, Param as Param4, ParseUUIDPipe as ParseUUIDPipe3, Patch as Patch4, Post as Post4, Query as Query3, UseGuards as UseGuards6 } from "@nestjs/common";
4344
4315
  function _ts_decorate20(decorators, target, key, desc) {
4345
4316
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4346
4317
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -4369,7 +4340,6 @@ function buildMarketingProjectionsController(guards) {
4369
4340
  }
4370
4341
  list(query) {
4371
4342
  return this.service.list({
4372
- projectKey: query.projectKey,
4373
4343
  targetType: query.targetType,
4374
4344
  targetVersionId: query.targetVersionId,
4375
4345
  locale: query.locale
@@ -4390,7 +4360,7 @@ function buildMarketingProjectionsController(guards) {
4390
4360
  };
4391
4361
  _ts_decorate20([
4392
4362
  Get6(),
4393
- _ts_param15(0, Query6()),
4363
+ _ts_param15(0, Query3()),
4394
4364
  _ts_metadata20("design:type", Function),
4395
4365
  _ts_metadata20("design:paramtypes", [
4396
4366
  typeof ListMarketingProjectionsQueryDto === "undefined" ? Object : ListMarketingProjectionsQueryDto
@@ -4450,7 +4420,7 @@ function buildMarketingProjectionsController(guards) {
4450
4420
  __name(buildMarketingProjectionsController, "buildMarketingProjectionsController");
4451
4421
 
4452
4422
  // src/catalog/dto/plans.dto.ts
4453
- import { IsInt as IsInt4, IsOptional as IsOptional5, IsString as IsString6, Matches as Matches6, Max as Max3, MaxLength as MaxLength5, Min as Min3, MinLength as MinLength3 } from "class-validator";
4423
+ import { IsInt as IsInt4, IsOptional as IsOptional5, IsString as IsString6, Matches as Matches5, Max as Max3, MaxLength as MaxLength5, Min as Min3, MinLength as MinLength3 } from "class-validator";
4454
4424
  function _ts_decorate21(decorators, target, key, desc) {
4455
4425
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4456
4426
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -4463,12 +4433,10 @@ function _ts_metadata21(k, v) {
4463
4433
  }
4464
4434
  __name(_ts_metadata21, "_ts_metadata");
4465
4435
  var KEY_PATTERN2 = /^[A-Z][A-Z0-9_]*$/;
4466
- var PROJECT_KEY_PATTERN6 = /^[a-z][a-z0-9-]*$/;
4467
4436
  var CreatePlanDto = class {
4468
4437
  static {
4469
4438
  __name(this, "CreatePlanDto");
4470
4439
  }
4471
- projectKey;
4472
4440
  planKey;
4473
4441
  label;
4474
4442
  description;
@@ -4477,15 +4445,7 @@ var CreatePlanDto = class {
4477
4445
  };
4478
4446
  _ts_decorate21([
4479
4447
  IsString6(),
4480
- Matches6(PROJECT_KEY_PATTERN6, {
4481
- message: 'projectKey must be kebab-case (e.g. "my-app")'
4482
- }),
4483
- MaxLength5(64),
4484
- _ts_metadata21("design:type", String)
4485
- ], CreatePlanDto.prototype, "projectKey", void 0);
4486
- _ts_decorate21([
4487
- IsString6(),
4488
- Matches6(KEY_PATTERN2, {
4448
+ Matches5(KEY_PATTERN2, {
4489
4449
  message: 'planKey must be SCREAMING_SNAKE_CASE (e.g. "STARTER")'
4490
4450
  }),
4491
4451
  MaxLength5(64),
@@ -4553,7 +4513,7 @@ _ts_decorate21([
4553
4513
  ], UpdatePlanDto.prototype, "sortOrder", void 0);
4554
4514
 
4555
4515
  // src/catalog/dto/plan-versions.dto.ts
4556
- import { IsArray as IsArray5, IsBoolean as IsBoolean4, IsISO8601, IsObject as IsObject4, IsOptional as IsOptional6, IsString as IsString7, IsUUID as IsUUID3, Matches as Matches7, MaxLength as MaxLength6, ValidateIf as ValidateIf2 } from "class-validator";
4516
+ import { IsArray as IsArray5, IsBoolean as IsBoolean4, IsISO8601, IsObject as IsObject4, IsOptional as IsOptional6, IsString as IsString7, IsUUID as IsUUID3, Matches as Matches6, MaxLength as MaxLength6, ValidateIf as ValidateIf2 } from "class-validator";
4557
4517
  function _ts_decorate22(decorators, target, key, desc) {
4558
4518
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4559
4519
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -4593,7 +4553,7 @@ _ts_decorate22([
4593
4553
  IsString7({
4594
4554
  each: true
4595
4555
  }),
4596
- Matches7(FEATURE_KEY_PATTERN2, {
4556
+ Matches6(FEATURE_KEY_PATTERN2, {
4597
4557
  each: true,
4598
4558
  message: "features entries must be SCREAMING_SNAKE_CASE"
4599
4559
  }),
@@ -4605,7 +4565,7 @@ _ts_decorate22([
4605
4565
  IsString7({
4606
4566
  each: true
4607
4567
  }),
4608
- Matches7(FEATURE_KEY_PATTERN2, {
4568
+ Matches6(FEATURE_KEY_PATTERN2, {
4609
4569
  each: true,
4610
4570
  message: "bundles entries must be SCREAMING_SNAKE_CASE"
4611
4571
  }),
@@ -4613,16 +4573,17 @@ _ts_decorate22([
4613
4573
  ], CreatePlanVersionDraftDto.prototype, "bundles", void 0);
4614
4574
  _ts_decorate22([
4615
4575
  IsObject4(),
4576
+ IsQuotaRecord(),
4616
4577
  _ts_metadata22("design:type", typeof Record === "undefined" ? Object : Record)
4617
4578
  ], CreatePlanVersionDraftDto.prototype, "quotas", void 0);
4618
4579
  _ts_decorate22([
4619
- Matches7(DECIMAL_PATTERN2, {
4580
+ Matches6(DECIMAL_PATTERN2, {
4620
4581
  message: 'monthlyNet must be a decimal with at most 2 fraction digits (e.g. "9.90")'
4621
4582
  }),
4622
4583
  _ts_metadata22("design:type", String)
4623
4584
  ], CreatePlanVersionDraftDto.prototype, "monthlyNet", void 0);
4624
4585
  _ts_decorate22([
4625
- Matches7(DECIMAL_PATTERN2, {
4586
+ Matches6(DECIMAL_PATTERN2, {
4626
4587
  message: "yearlyNet must be a decimal with at most 2 fraction digits"
4627
4588
  }),
4628
4589
  _ts_metadata22("design:type", String)
@@ -4677,7 +4638,7 @@ _ts_decorate22([
4677
4638
  IsString7({
4678
4639
  each: true
4679
4640
  }),
4680
- Matches7(FEATURE_KEY_PATTERN2, {
4641
+ Matches6(FEATURE_KEY_PATTERN2, {
4681
4642
  each: true
4682
4643
  }),
4683
4644
  _ts_metadata22("design:type", Array)
@@ -4688,7 +4649,7 @@ _ts_decorate22([
4688
4649
  IsString7({
4689
4650
  each: true
4690
4651
  }),
4691
- Matches7(FEATURE_KEY_PATTERN2, {
4652
+ Matches6(FEATURE_KEY_PATTERN2, {
4692
4653
  each: true
4693
4654
  }),
4694
4655
  _ts_metadata22("design:type", Array)
@@ -4696,16 +4657,17 @@ _ts_decorate22([
4696
4657
  _ts_decorate22([
4697
4658
  IsOptional6(),
4698
4659
  IsObject4(),
4660
+ IsQuotaRecord(),
4699
4661
  _ts_metadata22("design:type", typeof Record === "undefined" ? Object : Record)
4700
4662
  ], UpdatePlanVersionDraftDto.prototype, "quotas", void 0);
4701
4663
  _ts_decorate22([
4702
4664
  IsOptional6(),
4703
- Matches7(DECIMAL_PATTERN2),
4665
+ Matches6(DECIMAL_PATTERN2),
4704
4666
  _ts_metadata22("design:type", String)
4705
4667
  ], UpdatePlanVersionDraftDto.prototype, "monthlyNet", void 0);
4706
4668
  _ts_decorate22([
4707
4669
  IsOptional6(),
4708
- Matches7(DECIMAL_PATTERN2),
4670
+ Matches6(DECIMAL_PATTERN2),
4709
4671
  _ts_metadata22("design:type", String)
4710
4672
  ], UpdatePlanVersionDraftDto.prototype, "yearlyNet", void 0);
4711
4673
  _ts_decorate22([
@@ -4787,7 +4749,7 @@ _ts_decorate22([
4787
4749
  ], PublishPlanVersionDto.prototype, "validUntil", void 0);
4788
4750
 
4789
4751
  // src/catalog/plans.controller.ts
4790
- import { Body as Body6, Controller as Controller7, Delete as Delete4, Get as Get7, HttpCode as HttpCode4, HttpStatus as HttpStatus4, Inject as Inject16, Optional as Optional8, Param as Param5, ParseUUIDPipe as ParseUUIDPipe4, Patch as Patch5, Post as Post5, Query as Query7, UseGuards as UseGuards7 } from "@nestjs/common";
4752
+ import { Body as Body6, Controller as Controller7, Delete as Delete4, Get as Get7, HttpCode as HttpCode4, HttpStatus as HttpStatus4, Inject as Inject16, Optional as Optional8, Param as Param5, ParseUUIDPipe as ParseUUIDPipe4, Patch as Patch5, Post as Post5, Query as Query4, UseGuards as UseGuards7 } from "@nestjs/common";
4791
4753
  function _ts_decorate23(decorators, target, key, desc) {
4792
4754
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4793
4755
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -4817,15 +4779,15 @@ function buildPlansController(guards) {
4817
4779
  this.versionsService = versionsService;
4818
4780
  }
4819
4781
  // ─── Master operations ───
4820
- listPlans(projectKey, onlyPublished) {
4821
- return this.service.listPlans(projectKey, {
4782
+ listPlans(onlyPublished) {
4783
+ return this.service.listPlans({
4822
4784
  onlyPublished: onlyPublished === "true"
4823
4785
  });
4824
4786
  }
4825
4787
  // Static route BEFORE `@Get(':id')` — Fastify prioritizes static over
4826
4788
  // parametric; this makes the ordering additionally explicit.
4827
- getTenantCounts(projectKey) {
4828
- return this.service.getTenantCounts(projectKey);
4789
+ getTenantCounts() {
4790
+ return this.service.getTenantCounts();
4829
4791
  }
4830
4792
  getPlan(planId) {
4831
4793
  return this.service.getPlan(planId);
@@ -4874,22 +4836,17 @@ function buildPlansController(guards) {
4874
4836
  };
4875
4837
  _ts_decorate23([
4876
4838
  Get7(),
4877
- _ts_param16(0, Query7("projectKey")),
4878
- _ts_param16(1, Query7("onlyPublished")),
4839
+ _ts_param16(0, Query4("onlyPublished")),
4879
4840
  _ts_metadata23("design:type", Function),
4880
4841
  _ts_metadata23("design:paramtypes", [
4881
- String,
4882
4842
  String
4883
4843
  ]),
4884
4844
  _ts_metadata23("design:returntype", void 0)
4885
4845
  ], GeneratedPlansController.prototype, "listPlans", null);
4886
4846
  _ts_decorate23([
4887
4847
  Get7("tenant-counts"),
4888
- _ts_param16(0, Query7("projectKey")),
4889
4848
  _ts_metadata23("design:type", Function),
4890
- _ts_metadata23("design:paramtypes", [
4891
- String
4892
- ]),
4849
+ _ts_metadata23("design:paramtypes", []),
4893
4850
  _ts_metadata23("design:returntype", void 0)
4894
4851
  ], GeneratedPlansController.prototype, "getTenantCounts", null);
4895
4852
  _ts_decorate23([
@@ -4933,6 +4890,7 @@ function buildPlansController(guards) {
4933
4890
  ], GeneratedPlansController.prototype, "softDeletePlan", null);
4934
4891
  _ts_decorate23([
4935
4892
  Delete4(":id/purge"),
4893
+ EnforceMfa(),
4936
4894
  HttpCode4(HttpStatus4.NO_CONTENT),
4937
4895
  _ts_param16(0, Param5("id", new ParseUUIDPipe4())),
4938
4896
  _ts_metadata23("design:type", Function),
@@ -5029,6 +4987,7 @@ function buildPlanVersionsController(guards) {
5029
4987
  ], GeneratedPlanVersionsController.prototype, "updateDraft", null);
5030
4988
  _ts_decorate23([
5031
4989
  Post5(":id/publish"),
4990
+ EnforceMfa(),
5032
4991
  _ts_param16(0, Param5("id", new ParseUUIDPipe4())),
5033
4992
  _ts_param16(1, Body6()),
5034
4993
  _ts_metadata23("design:type", Function),
@@ -5050,6 +5009,7 @@ function buildPlanVersionsController(guards) {
5050
5009
  ], GeneratedPlanVersionsController.prototype, "discardDraft", null);
5051
5010
  _ts_decorate23([
5052
5011
  Post5(":id/terminate"),
5012
+ EnforceMfa(),
5053
5013
  _ts_param16(0, Param5("id", new ParseUUIDPipe4())),
5054
5014
  _ts_param16(1, Body6()),
5055
5015
  _ts_metadata23("design:type", Function),
@@ -5114,9 +5074,22 @@ var CatalogModule = class _CatalogModule {
5114
5074
  }
5115
5075
  }
5116
5076
  if (hasPublicMarketingCatalog && options.publicMarketingCatalog) {
5117
- controllers.push(buildPublicMarketingCatalogController(options.publicMarketingCatalog.guards, options.publicMarketingCatalog.projectKey, options.publicMarketingCatalog.currency, options.publicMarketingCatalog.vatRate));
5077
+ controllers.push(buildPublicMarketingCatalogController(options.publicMarketingCatalog.guards, options.publicMarketingCatalog.currency, options.publicMarketingCatalog.vatRate));
5118
5078
  }
5119
5079
  const providers = [
5080
+ // Always provided, `null` included: the service asks for it without
5081
+ // `@Optional()`, so a catalogue that publishes the answer cannot be
5082
+ // wired without one. `useExisting` rather than an instance, because
5083
+ // the source is a provider of the module the application wired for
5084
+ // payments — and where that module is out of scope, Nest says so at
5085
+ // boot instead of answering "no payment method is taken".
5086
+ options.publicMarketingCatalog?.newPaymentMethodsFrom ? {
5087
+ provide: NEW_PAYMENT_METHODS_SOURCE_TOKEN,
5088
+ useExisting: options.publicMarketingCatalog.newPaymentMethodsFrom
5089
+ } : {
5090
+ provide: NEW_PAYMENT_METHODS_SOURCE_TOKEN,
5091
+ useValue: null
5092
+ },
5120
5093
  asProvider(BUNDLE_REPOSITORY_TOKEN, options.bundleRepository),
5121
5094
  {
5122
5095
  provide: CATALOG_SERVICE_CONFIG_TOKEN,
@@ -5179,6 +5152,9 @@ CatalogModule = _ts_decorate24([
5179
5152
  ], CatalogModule);
5180
5153
 
5181
5154
  export {
5155
+ PLAN_VERSION_WINDOW_CODES,
5156
+ BUNDLE_VERSION_WINDOW_CODES,
5157
+ resolveValidityWindow,
5182
5158
  loadApprovedCatalogKeys,
5183
5159
  ADVISORY_STRICT_MODE_CODES,
5184
5160
  blockingStrictModeWarnings,
@@ -5189,18 +5165,19 @@ export {
5189
5165
  featureApprovalSignature,
5190
5166
  quotaApprovalSignature,
5191
5167
  CatalogEntriesService,
5168
+ defaultActorFromRequest,
5169
+ actorTagOf,
5170
+ WebAuditLogger,
5192
5171
  ListCatalogEntriesQueryDto,
5193
5172
  ReviewCatalogEntryDto,
5194
5173
  UpdateCatalogEntryI18nDto,
5195
5174
  SyncDiscoveryDto,
5196
5175
  buildCatalogEntriesController,
5197
5176
  PromotionsService,
5198
- ListPromotionsQueryDto,
5199
5177
  CreatePromotionDto,
5200
5178
  UpdatePromotionDto,
5201
5179
  buildPromotionsController,
5202
5180
  MarketingSettingsService,
5203
- ListMarketingSettingsQueryDto,
5204
5181
  UpdateMarketingSettingsDto,
5205
5182
  buildMarketingSettingsController,
5206
5183
  PublicMarketingCatalogService,