@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,21 +1,26 @@
1
1
  import {
2
2
  FEATURE_GUARD_MARKER,
3
+ PLAN_CATALOG_UNREADABLE_ERROR,
3
4
  PUBLIC_CATALOG_BUNDLE_REPOSITORY_TOKEN,
4
5
  PUBLIC_CATALOG_CATALOG_ENTRY_REPOSITORY_TOKEN,
5
- PUBLIC_CATALOG_PROJECT_KEY_TOKEN,
6
+ PlanCatalogValidationError,
6
7
  REQUIRE_FEATURE_KEY,
8
+ loadPlanCatalogFromString,
7
9
  periodEndAfter
8
- } from "./chunk-6Z7JR4EW.js";
10
+ } from "./chunk-M47BNEY2.js";
11
+ import {
12
+ EnforceMfa
13
+ } from "./chunk-34XVPR6P.js";
9
14
  import {
10
15
  EntitlementService,
11
16
  isLimitExceededError
12
- } from "./chunk-VIJ5NJWC.js";
17
+ } from "./chunk-G6EZWECL.js";
13
18
  import {
14
19
  ENTITLEMENT_SERVICE_TOKEN
15
20
  } from "./chunk-OUDSQ3PU.js";
16
21
  import {
17
22
  asProvider
18
- } from "./chunk-SABTXESR.js";
23
+ } from "./chunk-J7NJRK3K.js";
19
24
  import {
20
25
  __name
21
26
  } from "./chunk-SHUYVCID.js";
@@ -71,6 +76,8 @@ __name(assertOptimisticLockHeld, "assertOptimisticLockHeld");
71
76
  function decideRenewal(sub, now) {
72
77
  if (!sub.pendingPlanVersionId || !sub.pendingPlanVersionEffectiveAt) return "SKIP";
73
78
  if (sub.pendingPlanVersionEffectiveAt > now) return "SKIP";
79
+ const landedAt = sub.canceledEffectiveAt ?? sub.canceledAt;
80
+ if (landedAt !== null && landedAt <= now) return "SKIP";
74
81
  if (sub.pendingPlanVersionNonRegressive || sub.pendingPlanVersionAccepted) {
75
82
  return "ROLL_FORWARD";
76
83
  }
@@ -90,14 +97,16 @@ function clearPendingPlanVersionFields() {
90
97
  }
91
98
  __name(clearPendingPlanVersionFields, "clearPendingPlanVersionFields");
92
99
  function computeNextPeriod(sub, now) {
93
- if (sub.canceledAt !== null) return null;
94
100
  if (sub.currentPeriodEnd === null) return null;
95
101
  if (sub.currentPeriodEnd > now) return null;
102
+ const landedAt = sub.canceledEffectiveAt ?? sub.canceledAt;
103
+ if (landedAt !== null && landedAt <= now) return null;
96
104
  const newStart = sub.currentPeriodEnd;
97
- const newEnd = periodEndAfter(newStart, sub.billingCycle, now);
105
+ const newEnd = periodEndAfter(newStart, sub.billingCycle, now, sub.billingAnchorDay ?? void 0);
98
106
  return {
99
107
  currentPeriodStart: newStart,
100
- currentPeriodEnd: newEnd
108
+ currentPeriodEnd: newEnd,
109
+ minimumTermUntil: newEnd
101
110
  };
102
111
  }
103
112
  __name(computeNextPeriod, "computeNextPeriod");
@@ -261,12 +270,10 @@ var CatalogBundleUpsellResolver = class {
261
270
  __name(this, "CatalogBundleUpsellResolver");
262
271
  }
263
272
  bundleRepo;
264
- projectKey;
265
273
  catalogEntryRepo;
266
274
  currency;
267
- constructor(bundleRepo, projectKey, catalogEntryRepo = null, currency = null) {
275
+ constructor(bundleRepo, catalogEntryRepo = null, currency = null) {
268
276
  this.bundleRepo = bundleRepo;
269
- this.projectKey = projectKey;
270
277
  this.catalogEntryRepo = catalogEntryRepo;
271
278
  this.currency = currency;
272
279
  }
@@ -296,15 +303,12 @@ var CatalogBundleUpsellResolver = class {
296
303
  */
297
304
  async lookupUnmetRequires(featureKeys) {
298
305
  if (!this.catalogEntryRepo) return [];
299
- const rows = await this.catalogEntryRepo.listFeatures({
300
- projectKey: this.projectKey
301
- });
306
+ const rows = await this.catalogEntryRepo.listFeatures({});
302
307
  return collectUnsatisfiedRequires(featureKeys, buildFeatureRequiresIndex(rows));
303
308
  }
304
309
  /** Only published-and-live AND marketed — non-marketed bundles are not an offer. */
305
310
  async listLiveMarketedBundles() {
306
311
  const stems = await this.bundleRepo.list({
307
- projectKey: this.projectKey,
308
312
  excludeDeleted: true
309
313
  });
310
314
  const out = [];
@@ -327,15 +331,13 @@ var CatalogBundleUpsellResolver = class {
327
331
  CatalogBundleUpsellResolver = _ts_decorate2([
328
332
  Injectable2(),
329
333
  _ts_param2(0, Inject2(PUBLIC_CATALOG_BUNDLE_REPOSITORY_TOKEN)),
330
- _ts_param2(1, Inject2(PUBLIC_CATALOG_PROJECT_KEY_TOKEN)),
334
+ _ts_param2(1, Optional2()),
335
+ _ts_param2(1, Inject2(PUBLIC_CATALOG_CATALOG_ENTRY_REPOSITORY_TOKEN)),
331
336
  _ts_param2(2, Optional2()),
332
- _ts_param2(2, Inject2(PUBLIC_CATALOG_CATALOG_ENTRY_REPOSITORY_TOKEN)),
333
- _ts_param2(3, Optional2()),
334
- _ts_param2(3, Inject2(UPSELL_OFFER_CURRENCY_TOKEN)),
337
+ _ts_param2(2, Inject2(UPSELL_OFFER_CURRENCY_TOKEN)),
335
338
  _ts_metadata2("design:type", Function),
336
339
  _ts_metadata2("design:paramtypes", [
337
340
  typeof BundleRepository === "undefined" ? Object : BundleRepository,
338
- String,
339
341
  Object,
340
342
  Object
341
343
  ])
@@ -398,82 +400,6 @@ LimitExceededFilter = _ts_decorate3([
398
400
  Catch()
399
401
  ], LimitExceededFilter);
400
402
 
401
- // src/billing/plan-catalog-loader.ts
402
- import { readFileSync } from "fs";
403
- import { resolve as resolvePath } from "path";
404
- import yaml from "js-yaml";
405
- import Ajv2020 from "ajv/dist/2020.js";
406
- import addFormats from "ajv-formats";
407
- import { planCatalogSchema } from "@saasicat/spec";
408
- var PlanCatalogValidationError = class extends Error {
409
- static {
410
- __name(this, "PlanCatalogValidationError");
411
- }
412
- source;
413
- errors;
414
- constructor(source, errors) {
415
- const messages = errors.map((e) => `${e.instancePath || "/"}: ${e.message ?? "unknown"}`).join("\n ");
416
- super(`Plan catalog validation failed for ${source}:
417
- ${messages}`), this.source = source, this.errors = errors;
418
- this.name = "PlanCatalogValidationError";
419
- }
420
- };
421
- function loadPlanCatalogFromFile(opts) {
422
- const absolutePath = resolvePath(opts.path);
423
- const raw = readFileSync(absolutePath, "utf-8");
424
- return loadPlanCatalogFromString(raw, {
425
- source: absolutePath,
426
- crossFieldChecks: opts.crossFieldChecks ?? true
427
- });
428
- }
429
- __name(loadPlanCatalogFromFile, "loadPlanCatalogFromFile");
430
- function loadPlanCatalogFromString(yamlContent, opts) {
431
- const parsed = yaml.load(yamlContent);
432
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
433
- throw new Error(`The YAML content of ${opts.source} is not an object`);
434
- }
435
- const ajv = new Ajv2020({
436
- strict: false,
437
- allErrors: true
438
- });
439
- addFormats.default(ajv);
440
- const validate = ajv.compile(planCatalogSchema);
441
- if (!validate(parsed)) {
442
- throw new PlanCatalogValidationError(opts.source, validate.errors ?? []);
443
- }
444
- const catalog = parsed;
445
- if (opts.crossFieldChecks ?? true) {
446
- validateConsistency(catalog, opts.source);
447
- }
448
- return catalog;
449
- }
450
- __name(loadPlanCatalogFromString, "loadPlanCatalogFromString");
451
- function validateConsistency(catalog, source) {
452
- const errors = [];
453
- const declaredFeatureKeys = new Set((catalog.features ?? []).map((f) => f.key));
454
- const planIds = /* @__PURE__ */ new Set();
455
- for (const plan of catalog.plans ?? []) {
456
- if (planIds.has(plan.id)) {
457
- errors.push(`plans[].id: duplicate plan id "${plan.id}"`);
458
- }
459
- planIds.add(plan.id);
460
- if (catalog.features) {
461
- for (const fk of plan.features) {
462
- if (!declaredFeatureKeys.has(fk)) {
463
- errors.push(`plans[id=${plan.id}].features: Unknown featureKey "${fk}" \u2014 not declared in catalog.features`);
464
- }
465
- }
466
- }
467
- }
468
- if (errors.length > 0) {
469
- const err = new Error(`Plan catalog consistency error in ${source}:
470
- ${errors.join("\n ")}`);
471
- err.name = "PlanCatalogConsistencyError";
472
- throw err;
473
- }
474
- }
475
- __name(validateConsistency, "validateConsistency");
476
-
477
403
  // src/billing/plan-catalog-importer.service.ts
478
404
  import { Inject as Inject3, Injectable as Injectable3 } from "@nestjs/common";
479
405
  function _ts_decorate4(decorators, target, key, desc) {
@@ -529,7 +455,6 @@ var PlanCatalogImporterService = class {
529
455
  };
530
456
  for (const feature of catalog.features ?? []) {
531
457
  const result = await this.sink.upsertFeatureCatalogEntry({
532
- projectKey: catalog.projectKey,
533
458
  featureKey: feature.key,
534
459
  label: feature.label,
535
460
  icon: feature.icon,
@@ -541,7 +466,6 @@ var PlanCatalogImporterService = class {
541
466
  }
542
467
  for (const plan of catalog.plans ?? []) {
543
468
  const planResult = await this.sink.upsertPlan({
544
- projectKey: catalog.projectKey,
545
469
  planKey: plan.id,
546
470
  label: plan.name ?? plan.id,
547
471
  description: plan.tagline ?? null
@@ -552,13 +476,17 @@ var PlanCatalogImporterService = class {
552
476
  report.warnings.push(`Plan '${plan.id}' has no monthlyNet \u2014 skipped (on-request plans need a manual plan version).`);
553
477
  continue;
554
478
  }
479
+ if (plan.yearlyNet === null || plan.yearlyNet === void 0) {
480
+ report.warnings.push(`Plan '${plan.id}' has no yearlyNet \u2014 skipped (a plan version needs a yearly price; set one in the catalog or publish the version manually).`);
481
+ continue;
482
+ }
555
483
  const versionResult = await this.sink.upsertPlanVersion({
556
484
  planKey: plan.id,
557
485
  version: 1,
558
486
  features: plan.features,
559
487
  quotas: plan.quotas,
560
488
  monthlyNet: plan.monthlyNet.toFixed(2),
561
- yearlyNet: (plan.yearlyNet ?? plan.monthlyNet * 10).toFixed(2),
489
+ yearlyNet: plan.yearlyNet.toFixed(2),
562
490
  marketed: plan.marketed ?? true,
563
491
  publish: true,
564
492
  changeNote: IMPORT_CHANGE_NOTE
@@ -582,8 +510,9 @@ PlanCatalogImporterService = _ts_decorate4([
582
510
  import { Module } from "@nestjs/common";
583
511
 
584
512
  // src/billing/plan-catalog-importer.controller.ts
585
- import { Body, Controller, Inject as Inject4, Post, UseGuards } from "@nestjs/common";
513
+ import { BadRequestException, Body, Controller, Inject as Inject4, Post, UseGuards } from "@nestjs/common";
586
514
  import { IsBoolean, IsOptional, IsString, MinLength } from "class-validator";
515
+ import { CATALOG_ERROR_CODES } from "@saasicat/core";
587
516
  function _ts_decorate5(decorators, target, key, desc) {
588
517
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
589
518
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -618,6 +547,15 @@ _ts_decorate5([
618
547
  IsBoolean(),
619
548
  _ts_metadata4("design:type", Boolean)
620
549
  ], PlanCatalogImportDto.prototype, "crossFieldChecks", void 0);
550
+ var UNREADABLE_ERROR_NAMES = /* @__PURE__ */ new Set([
551
+ "YAMLException",
552
+ "YAMLParseError",
553
+ PLAN_CATALOG_UNREADABLE_ERROR
554
+ ]);
555
+ function isUnreadableDocument(error) {
556
+ return error instanceof Error && UNREADABLE_ERROR_NAMES.has(error.name);
557
+ }
558
+ __name(isUnreadableDocument, "isUnreadableDocument");
621
559
  function buildPlanCatalogImporterController(guards) {
622
560
  let GeneratedPlanCatalogImporterController = class GeneratedPlanCatalogImporterController {
623
561
  static {
@@ -627,14 +565,45 @@ function buildPlanCatalogImporterController(guards) {
627
565
  constructor(service) {
628
566
  this.service = service;
629
567
  }
568
+ /**
569
+ * A body that does not parse or does not validate is the caller's
570
+ * mistake, so it answers 400.
571
+ *
572
+ * Without this it answered 500: the loader throws a plain `Error` for
573
+ * unparseable YAML and a `PlanCatalogValidationError` for a schema or
574
+ * cross-field violation, and neither is an HTTP exception. A caller
575
+ * cannot tell a bad upload from a broken server that way, and the one
576
+ * they can fix is the one that looked unfixable.
577
+ */
630
578
  async import(dto) {
631
- return this.service.importFromYaml(dto.yamlContent, {
632
- crossFieldChecks: dto.crossFieldChecks
633
- });
579
+ try {
580
+ return await this.service.importFromYaml(dto.yamlContent, {
581
+ crossFieldChecks: dto.crossFieldChecks
582
+ });
583
+ } catch (error) {
584
+ if (error instanceof PlanCatalogValidationError) {
585
+ throw new BadRequestException({
586
+ code: CATALOG_ERROR_CODES.PLAN_CATALOG_INVALID,
587
+ message: error.message,
588
+ params: {
589
+ message: error.message,
590
+ errors: error.errors
591
+ }
592
+ });
593
+ }
594
+ if (isUnreadableDocument(error)) {
595
+ throw new BadRequestException({
596
+ code: CATALOG_ERROR_CODES.PLAN_CATALOG_UNREADABLE,
597
+ message: error.message
598
+ });
599
+ }
600
+ throw error;
601
+ }
634
602
  }
635
603
  };
636
604
  _ts_decorate5([
637
605
  Post("import"),
606
+ EnforceMfa(),
638
607
  _ts_param4(0, Body()),
639
608
  _ts_metadata4("design:type", Function),
640
609
  _ts_metadata4("design:paramtypes", [
@@ -724,7 +693,6 @@ var ConfiguratorCatalogBuilder = class {
724
693
  m
725
694
  ]));
726
695
  return {
727
- cycleDiscount: marketing.getCycleDiscount(),
728
696
  currency: marketing.getCurrency(),
729
697
  vatRate: marketing.getVatRate(),
730
698
  models: planRows.filter((row) => row.marketed).map((row) => buildModel(row, planMarketingByPlanId)).filter((m) => m !== null)
@@ -779,9 +747,6 @@ export {
779
747
  FeatureGuard,
780
748
  CatalogBundleUpsellResolver,
781
749
  LimitExceededFilter,
782
- PlanCatalogValidationError,
783
- loadPlanCatalogFromFile,
784
- loadPlanCatalogFromString,
785
750
  PLAN_CATALOG_IMPORT_SINK_TOKEN,
786
751
  PlanCatalogImporterService,
787
752
  PlanCatalogImportDto,
@@ -2,7 +2,7 @@ import {
2
2
  DISCOVERY_APP_INFO_TOKEN,
3
3
  DISCOVERY_SNAPSHOT_PATH_TOKEN,
4
4
  DiscoveryScanner
5
- } from "./chunk-WXYJHZCN.js";
5
+ } from "./chunk-JVAEKTJ4.js";
6
6
  import {
7
7
  DISCOVERY_SNAPSHOT_TOKEN
8
8
  } from "./chunk-QUZU2M33.js";
@@ -0,0 +1,10 @@
1
+ // src/admin/admin.tokens.ts
2
+ var MFA_PORT_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/nest/MfaPort");
3
+ var AUDIT_PORT_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/nest/AuditPort");
4
+ var RLS_BYPASS_PORT_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/nest/RlsBypassPort");
5
+
6
+ export {
7
+ MFA_PORT_TOKEN,
8
+ AUDIT_PORT_TOKEN,
9
+ RLS_BYPASS_PORT_TOKEN
10
+ };
@@ -0,0 +1,98 @@
1
+ import {
2
+ assertTaxRatePercent,
3
+ grossFromNet
4
+ } from "./chunk-6PJE7EWO.js";
5
+ import {
6
+ __name
7
+ } from "./chunk-SHUYVCID.js";
8
+
9
+ // src/billing/plan-helpers.ts
10
+ import { BILLING_ERROR_CODES } from "@saasicat/core";
11
+ function findPlan(catalog, planId) {
12
+ return (catalog.plans ?? []).find((p) => p.id === planId);
13
+ }
14
+ __name(findPlan, "findPlan");
15
+ function getPlanOrThrow(catalog, planId) {
16
+ const plan = findPlan(catalog, planId);
17
+ if (!plan) {
18
+ throw new Error(`Plan "${planId}" is not in the catalog`);
19
+ }
20
+ return plan;
21
+ }
22
+ __name(getPlanOrThrow, "getPlanOrThrow");
23
+ function getMarketedPlans(catalog) {
24
+ return (catalog.plans ?? []).filter((p) => p.marketed !== false);
25
+ }
26
+ __name(getMarketedPlans, "getMarketedPlans");
27
+ function getPlanPriceNet(catalog, planId, cycle) {
28
+ const plan = findPlan(catalog, planId);
29
+ return plan ? listPriceNet(plan, cycle) : null;
30
+ }
31
+ __name(getPlanPriceNet, "getPlanPriceNet");
32
+ function listPriceNet(plan, cycle) {
33
+ if (plan.marketed === false) return null;
34
+ const net = cycle === "YEARLY" ? plan.yearlyNet : plan.monthlyNet;
35
+ return net ?? null;
36
+ }
37
+ __name(listPriceNet, "listPriceNet");
38
+ function isPlanNotSoldInCycle(plan, cycle) {
39
+ if (plan.marketed === false) return false;
40
+ return (cycle === "YEARLY" ? plan.yearlyNet : plan.monthlyNet) == null;
41
+ }
42
+ __name(isPlanNotSoldInCycle, "isPlanNotSoldInCycle");
43
+ function planNotSoldInCycle(plan, cycle) {
44
+ const planName = plan.name ?? plan.id;
45
+ return {
46
+ code: BILLING_ERROR_CODES.PLAN_NOT_SOLD_IN_CYCLE,
47
+ message: `${planName} has no price for this billing rhythm and cannot be booked in it.`,
48
+ params: {
49
+ planName,
50
+ planKey: plan.id,
51
+ billingCycle: cycle
52
+ }
53
+ };
54
+ }
55
+ __name(planNotSoldInCycle, "planNotSoldInCycle");
56
+ function getPlanPriceGross(catalog, planId, cycle, vatRate) {
57
+ const net = getPlanPriceNet(catalog, planId, cycle);
58
+ if (net === null) return null;
59
+ const rate = vatRate ?? catalog.vatRate;
60
+ assertTaxRatePercent(vatRate === void 0 ? "catalog.vatRate" : "vatRate", rate);
61
+ return grossFromNet(net, rate);
62
+ }
63
+ __name(getPlanPriceGross, "getPlanPriceGross");
64
+ function isFeatureInPlan(catalog, planId, featureKey) {
65
+ const plan = findPlan(catalog, planId);
66
+ if (!plan) return false;
67
+ return plan.features.includes(featureKey);
68
+ }
69
+ __name(isFeatureInPlan, "isFeatureInPlan");
70
+ function getPlanQuota(catalog, planId, quotaKey) {
71
+ const plan = findPlan(catalog, planId);
72
+ return plan?.quotas[quotaKey];
73
+ }
74
+ __name(getPlanQuota, "getPlanQuota");
75
+ function getActiveFeatureKeys(catalog) {
76
+ return (catalog.features ?? []).filter((f) => !f.plannedOnly).map((f) => f.key);
77
+ }
78
+ __name(getActiveFeatureKeys, "getActiveFeatureKeys");
79
+ function isFeaturePlannedOnly(catalog, featureKey) {
80
+ const def = (catalog.features ?? []).find((f) => f.key === featureKey);
81
+ return def?.plannedOnly === true;
82
+ }
83
+ __name(isFeaturePlannedOnly, "isFeaturePlannedOnly");
84
+
85
+ export {
86
+ findPlan,
87
+ getPlanOrThrow,
88
+ getMarketedPlans,
89
+ getPlanPriceNet,
90
+ listPriceNet,
91
+ isPlanNotSoldInCycle,
92
+ planNotSoldInCycle,
93
+ getPlanPriceGross,
94
+ isFeatureInPlan,
95
+ getPlanQuota,
96
+ getActiveFeatureKeys,
97
+ isFeaturePlannedOnly
98
+ };