@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,25 +1,76 @@
1
- import { BillingCycle, FeatureKey, UpsellOfferResolver, BundleRepository, CatalogEntryRepository, UpsellOffer, PlanCatalog, PlanCatalogReadSink, PlanCatalogReadSnapshot, PlanCatalogImportSink, PlanCatalogImportReport, PlanId, PlanDef, QuotaKey, MarketingTopFeature, FeatureUiRegistry, MarketingProjectionRepository, ConfiguratorSourcesLookup, ConfiguratorMarketingProvider, ConfiguratorCatalog } from '@saasicat/core';
2
- export { BundleVersionFields, ChangeDirection, DiffResult, PlanVersionFields, VersionChange, VersionChangeDirection, classifyBundleVersionDiff, classifyPlanDiff } from '@saasicat/core';
3
- import { F as FEATURE_GUARD_MARKER, f as ContractFreezePort, g as ContractFreezeSourcePort } from '../tenant-billing.module-BznC3XtW.cjs';
4
- export { A as AUDIT_CONTEXT_RESOLVER_TOKEN, a as AuditContextResolver, b as AuthGuardList, C as CONTRACT_FREEZE_PORT_TOKEN, c as CONTRACT_FREEZE_PROJECT_KEY_TOKEN, d as CONTRACT_FREEZE_SOURCE_PORT_TOKEN, e as ContractFreezeBundleSnapshot, D as DuePendingPlanChange, M as MarkedFeatureGuard, P as PENDING_PLAN_QUERY_PORT_TOKEN, h as PendingPlanQueryPort, S as SELF_SERVICE_BLOCKED_BUNDLES_TOKEN, i as SELF_SERVICE_BLOCKED_PLANS_TOKEN, j as SUBSCRIPTION_USAGE_PORT_TOKEN, k as SUBSCRIPTION_WRITE_PORT_TOKEN, l as SelfServiceBlockedBundles, m as SelfServiceBlockedPlans, n as SubscriptionBundleControllerOptions, o as SubscriptionBundleModule, p as SubscriptionBundleModuleOptions, T as TENANT_AUTH_GUARDS_TOKEN, q as TENANT_ID_RESOLVER_TOKEN, r as TRIAL_PROJECTION_PORT_TOKEN, s as TenantBillingModule, t as TenantBillingModuleOptions, u as TenantIdResolver, v as TrialProjectionInput, w as TrialProjectionPort, U as USAGE_SNAPSHOT_PORT_TOKEN, x as USER_EMAIL_RESOLVER_TOKEN, y as USER_ID_RESOLVER_TOKEN, z as UserEmailResolver, B as UserIdResolver, E as isPlatformFeatureGuard } from '../tenant-billing.module-BznC3XtW.cjs';
1
+ import { BillingCycle, BundleVersionRow, FeatureKey, UpsellOfferResolver, BundleRepository, CatalogEntryRepository, UpsellOffer, PlanCatalogSettings, PlanCatalogReadSink, PlanCatalog, PlanCatalogReadSnapshot, PlanCatalogImportSink, PlanCatalogImportReport, PlanId, PlanDef, QuotaKey, MarketingTopFeature, FeatureUiRegistry, MarketingProjectionRepository, TenantSubscriptionWritePort, ConfiguratorSourcesLookup, ConfiguratorMarketingProvider, ConfiguratorCatalog } from '@saasicat/core';
2
+ export { BundleVersionFields, CancellationNoticePeriods, ChangeDirection, DiffResult, PlanVersionFields, SelfServiceBlockedPlans, VersionChange, VersionChangeDirection, classifyBundleVersionDiff, classifyPlanDiff } from '@saasicat/core';
3
+ export { A as AddBundleToSubscriptionInput, B as BundlePreviewSnapshot, C as CancelBundleFromSubscriptionInput, a as CancelSubscriptionDto, b as CancellableSubscription, c as CancellationDecision, d as CancellationInput, e as ChangePlanDto, f as CompleteOnboardingSubscriptionDto, g as ComposedTenantAuthGuard, h as CycleDirection, L as LimitsCheckRow, N as NO_NOTICE_PERIOD, P as PendingPlanMaterializationService, i as PlanChangeContext, j as PlanChangePreviewDto, k as PlanChangePreviewIssue, l as PlanChangePreviewService, m as PlanChangeType, n as PlanDirection, o as PlanSnapshotDto, p as PreviewPlanChangeDto, q as ProrationDto, r as ProrationInput, R as RedundantFeatureHint, S as SubscriptionBundleAddPreviewDto, s as SubscriptionBundleCancelPreviewDto, t as SubscriptionBundleConfig, u as SubscriptionBundlePreviewContext, v as SubscriptionBundlePreviewIssue, w as SubscriptionBundlePreviewService, x as SubscriptionBundlesService, T as TenantAdminGuard, y as TenantBillingController, U as UsageResponse, z as addMonths, D as clampToParent, E as computeProration, F as decideCancellation, G as decideCancellationFor, H as noticeDaysFor, I as resolveBundleCancelEffectiveAt } from '../tenant-billing.controller-DKbAqTbM.cjs';
4
+ import { F as FEATURE_GUARD_MARKER, d as ContractFreezePort, e as ContractFreezeSourcePort } from '../tenant-billing.module-ClbjzocC.cjs';
5
+ export { A as AjvErrorLike, C as CONTRACT_FREEZE_PORT_TOKEN, b as CONTRACT_FREEZE_SOURCE_PORT_TOKEN, c as ContractFreezeBundleSnapshot, E as EnvironmentVariables, f as LoadPlanCatalogFromStringOptions, L as LoadPlanCatalogOptions, M as MarkedFeatureGuard, P as PLAN_CATALOG_UNREADABLE_ERROR, g as PlanCatalogValidationError, h as SELF_SERVICE_BLOCKED_BUNDLES_TOKEN, i as SELF_SERVICE_BLOCKED_PLANS_TOKEN, j as SelfServiceBlockedBundles, a as SubscriptionBundleControllerOptions, k as SubscriptionBundleModule, S as SubscriptionBundleModuleOptions, l as TenantBillingModule, T as TenantBillingModuleOptions, m as catalogSource, n as isPlatformFeatureGuard, o as loadPlanCatalogFromFile, p as loadPlanCatalogFromString } from '../tenant-billing.module-ClbjzocC.cjs';
5
6
  import * as _nestjs_common from '@nestjs/common';
6
7
  import { CanActivate, ExecutionContext, ArgumentsHost, Type, DynamicModule, ForwardReference, Provider } from '@nestjs/common';
7
8
  export { Type as NestType } from '@nestjs/common';
8
9
  import { Reflector, BaseExceptionFilter } from '@nestjs/core';
9
- import { c as EntitlementService } from '../aggregation-B3CD0v_d.cjs';
10
+ import { c as EntitlementService } from '../aggregation-OpnhwzF5.cjs';
10
11
  import { P as ProviderSpec } from '../di-CcNeq9v-.cjs';
11
- export { A as AddBundleToSubscriptionInput, B as BundlePreviewSnapshot, C as CancelBundleFromSubscriptionInput, a as CancelSubscriptionDto, b as ChangePlanDto, c as CompleteOnboardingSubscriptionDto, d as ComposedTenantAuthGuard, L as LimitsCheckRow, P as PendingPlanMaterializationService, e as PlanChangeContext, f as PlanChangePreviewDto, g as PlanChangePreviewIssue, h as PlanChangePreviewService, i as PlanChangeType, j as PlanSnapshotDto, k as PreviewPlanChangeDto, l as ProrationDto, m as ProrationInput, R as RedundantFeatureHint, S as SubscriptionBundleAddPreviewDto, n as SubscriptionBundleCancelPreviewDto, o as SubscriptionBundleConfig, p as SubscriptionBundlePreviewContext, q as SubscriptionBundlePreviewIssue, r as SubscriptionBundlePreviewService, s as SubscriptionBundlesService, T as TenantAdminGuard, t as TenantBillingController, U as UsageResponse, u as addMonths, v as computeProration, w as resolveBundleCancelEffectiveAt, x as resolveBundlePriceNet } from '../tenant-billing.controller-DS8kB8a7.cjs';
12
- import { S as SubscriptionContractService } from '../subscription-contract.service-hD87MKyg.cjs';
13
- import '../plan-resolution-Cgo_TR1H.cjs';
14
- import '../promo.service-BJbKAmw3.cjs';
12
+ import { P as PlanCatalogSource } from '../plan-catalog-source-DWe-BGY1.cjs';
13
+ export { a as PlanCatalogOrigin, g as givenPlanCatalogSource } from '../plan-catalog-source-DWe-BGY1.cjs';
14
+ import { d as AuthGuardList } from '../tenant-billing.tokens-G-1xOlMr.cjs';
15
+ export { c as AUDIT_CONTEXT_RESOLVER_TOKEN, A as AuditContextResolver, B as BILLING_PERMISSION_GUARDS_TOKEN, C as CANCELLATION_NOTICE_DAYS_TOKEN, D as DuePendingPlanChange, e as PENDING_PLAN_QUERY_PORT_TOKEN, P as PendingPlanQueryPort, S as SUBSCRIPTION_USAGE_PORT_TOKEN, f as SUBSCRIPTION_WRITE_PORT_TOKEN, g as TENANT_AUTH_GUARDS_TOKEN, h as TENANT_ID_RESOLVER_TOKEN, i as TRIAL_PROJECTION_PORT_TOKEN, T as TenantIdResolver, j as TrialProjectionInput, a as TrialProjectionPort, k as USAGE_SNAPSHOT_PORT_TOKEN, l as USER_EMAIL_RESOLVER_TOKEN, m as USER_ID_RESOLVER_TOKEN, U as UserEmailResolver, b as UserIdResolver } from '../tenant-billing.tokens-G-1xOlMr.cjs';
16
+ import { S as SubscriptionContractService } from '../subscription-contract.service-CfNApLuP.cjs';
17
+ import '../promo.service-BYBu6dy3.cjs';
15
18
  import '../admin-audit.service-4P3Djdxk.cjs';
19
+ import '../contract-line-item-money-B0Z3Ogur.cjs';
20
+ import '../plan-resolution-CwUy_OqC.cjs';
21
+ import '../subscriber.service-5W6Gi-1B.cjs';
16
22
 
23
+ /**
24
+ * One period onwards, on `anchorDay` where the month has one.
25
+ *
26
+ * The anchor is the day the subscription is billed on, and it survives months
27
+ * that are too short for it. Without one, this function has to read the day
28
+ * from its own input — and its own input is the previous clamped result, so a
29
+ * February eats the anchor and never gives it back: 31 January became 28
30
+ * February and then 28 March, 28 April, 28 May, for the rest of the
31
+ * subscription's life. Three days lost once, invisibly, and every later date
32
+ * measured from the wrong one.
33
+ *
34
+ * Clamping applies to the step's OUTPUT, never to the next step's input. A
35
+ * subscription anchored on the 31st is billed on the 28th in February and on
36
+ * the 31st again in March; one anchored on the 30th is billed on the 30th in
37
+ * October, not the 31st. The anchor is a day number, not "the end of the
38
+ * month".
39
+ *
40
+ * Omitting it keeps the old reading — the day of `d` — which is right wherever
41
+ * a caller has only one step to take and `d` is itself the anchor.
42
+ */
43
+ declare function advanceOneCycle(d: Date, cycle: BillingCycle, anchorDay?: number): Date;
44
+ /**
45
+ * One period backwards, on `anchorDay` where the month has one.
46
+ *
47
+ * The mirror of `advanceOneCycle`, and it exists for one question: how long is
48
+ * the full period that ends here? A short first period is charged pro rata
49
+ * against a whole cycle, and a whole cycle is only measurable from both of its
50
+ * ends. Reading its length from the calendar month instead is wrong wherever
51
+ * the anchor is not the 1st — a period ending on 28 February with an anchor of
52
+ * 31 runs from 31 January, which is 28 days, not the 31 that February's
53
+ * neighbour would suggest.
54
+ *
55
+ * The same clamp rule applies, and applies to this step's output: from 31
56
+ * March backwards on anchor 31 is 28 February, and from there backwards again
57
+ * is 31 January, not 28 January. The anchor is never consumed.
58
+ */
59
+ declare function retreatOneCycle(d: Date, cycle: BillingCycle, anchorDay?: number): Date;
60
+ /**
61
+ * The day of the month a subscription is billed on.
62
+ *
63
+ * Read from whichever date opened the current run of periods — the start of the
64
+ * window, not the end of the last one, because the end has already been through
65
+ * a clamp.
66
+ */
67
+ declare function billingAnchorDay(windowStart: Date): number;
17
68
  /**
18
69
  * Finds the next period boundary that lies strictly **after** `after`.
19
70
  * Iterates from `startedAt` (fallback: `after`) by +1 cycle each time, until
20
71
  * the result is greater than `after`.
21
72
  */
22
- declare function periodEndAfter(startedAt: Date | null, cycle: BillingCycle, after: Date): Date;
73
+ declare function periodEndAfter(startedAt: Date | null, cycle: BillingCycle, after: Date, anchorDay?: number): Date;
23
74
  /**
24
75
  * Returns the initial period window for a subscription
25
76
  * (`currentPeriodStart`/`currentPeriodEnd`). `start` is `startedAt`,
@@ -42,7 +93,203 @@ declare function initialPeriodWindow(startedAt: Date, cycle: BillingCycle): {
42
93
  *
43
94
  * Spec: ROADMAP §2 no. 3 (advance-warning period), §6.1 (time-based selection).
44
95
  */
45
- declare function periodEndWithMinLead(startedAt: Date | null, cycle: BillingCycle, now: Date, minLeadDays?: number): Date;
96
+ declare function periodEndWithMinLead(startedAt: Date | null, cycle: BillingCycle, now: Date, minLeadDays?: number, anchorDay?: number): Date;
97
+
98
+ /** What a caller knows about the plan's billing rhythm, before resolving it. */
99
+ interface PlanAnchorSource {
100
+ /** The stored anchor, where the subscription has one. */
101
+ billingAnchorDay?: number | null;
102
+ /** The date that opened the current run of periods. */
103
+ currentPeriodStart?: Date | null;
104
+ /** What opened it before the first window was written. */
105
+ startedAt?: Date | null;
106
+ }
107
+ /**
108
+ * The day of the month the plan is billed on, resolved once for everybody.
109
+ *
110
+ * Every caller that quotes a bundle and every caller that books one has to
111
+ * reach the same answer, or the preview describes a different contract from the
112
+ * one written — and this is the field that decides the difference. It happened:
113
+ * the preview derived the day from the window start while the booking left the
114
+ * value null and let the arithmetic read the window END, so for a 31 January to
115
+ * 28 February window the preview quoted a period ending on the 31st and the
116
+ * booking stored the 28th, after which the two renewed on different days
117
+ * forever.
118
+ *
119
+ * The order is authority, not convenience. The stored anchor is the answer
120
+ * where it exists. Failing that, the day that OPENED the current window —
121
+ * never the day that closed it, because a closing day has already been through
122
+ * a clamp and reading it is how a subscription billed on the 31st ends up
123
+ * billed on the 28th for the rest of its life.
124
+ */
125
+ declare function resolvePlanAnchorDay(source: PlanAnchorSource): number | null;
126
+ interface BundleFirstPeriodInput {
127
+ /** When the bundle was booked. */
128
+ startedAt: Date;
129
+ /** The bundle's own cycle — never longer than the plan's. */
130
+ cycle: BillingCycle;
131
+ /** When the plan's current period ends, if it has one. */
132
+ planPeriodEnd: Date | null;
133
+ /** The day of the month the plan is billed on, if it is known. */
134
+ planAnchorDay: number | null;
135
+ }
136
+ /**
137
+ * The end of a bundle's first period: short, and landing on the plan's day.
138
+ *
139
+ * Null where the plan has no period to align to — a trial, or a subscription
140
+ * still waiting for sales. A bundle booked there has no period of its own
141
+ * either, rather than one invented from the booking date.
142
+ */
143
+ declare function bundleFirstPeriodEnd(input: BundleFirstPeriodInput): Date | null;
144
+ /**
145
+ * The start of the full cycle that the first period is a part of.
146
+ *
147
+ * Not the booking date: the first period is short, and a short period is
148
+ * charged pro rata against a **whole** cycle. That whole cycle ends where the
149
+ * first period ends and begins one cycle before it — so a monthly bundle
150
+ * booked on 21 February against a plan anchored on the 31st is charged for
151
+ * 7 of the 28 days between 31 January and 28 February, not for 7 of 31, and
152
+ * not for the 160 days left of the plan's own yearly period.
153
+ *
154
+ * Prorating against the plan's period was the shape this used to have, and it
155
+ * is wrong by the width of the difference between the two cycles: a monthly
156
+ * bundle on a yearly plan was charged a fraction of a *year* at a monthly
157
+ * price.
158
+ */
159
+ declare function bundleFirstPeriodStart(firstPeriodEnd: Date, cycle: BillingCycle, planAnchorDay: number | null): Date;
160
+ /**
161
+ * The end of every period after the first: one cycle on, still on the plan's
162
+ * day.
163
+ */
164
+ declare function bundleNextPeriodEnd(currentPeriodEnd: Date, cycle: BillingCycle, planAnchorDay: number | null): Date;
165
+ /**
166
+ * Whether a bundle may be billed on `bundleCycle` beside a plan on `planCycle`.
167
+ *
168
+ * A bundle may run in a shorter rhythm than its plan, never a longer one. A
169
+ * yearly bundle on a monthly plan has no boundary to meet — the plan's periods
170
+ * end twelve times before the bundle's first one does, and every one of those
171
+ * ends is a moment the plan could stop and leave the bundle committed with
172
+ * nothing to grant.
173
+ *
174
+ * The shorter direction is fine and is the interesting case: a monthly bundle
175
+ * on a yearly plan simply lands on the plan's day every month, and on the
176
+ * plan's own boundary in the month the plan ends.
177
+ */
178
+ /**
179
+ * How long a booking commits the tenant when nobody says otherwise: **not at
180
+ * all**.
181
+ *
182
+ * An add-on can be cancelled at any time up to the moment its next period
183
+ * begins, and the cancellation takes effect at the end of the period it is in.
184
+ * The tenant pays for the period they are in, it ends normally, and nothing
185
+ * has to be paid back — which is the whole reason the rule is shaped this way.
186
+ *
187
+ * It was 12 until 2026-08-27, applied to every booking without an operator
188
+ * doing anything, and it made that rule impossible: a cancellation lands at
189
+ * `max(currentPeriodEnd, minimumTermEndsAt)`, so a monthly add-on booked today
190
+ * could not be cancelled to next month. On a yearly plan the term even
191
+ * outlasted the bundle's own last period. An operator who wants a commitment
192
+ * configures one; nobody gets one by default.
193
+ */
194
+ declare const DEFAULT_BUNDLE_MINIMUM_TERM_MONTHS = 0;
195
+ declare function bundleCycleFitsPlan(bundleCycle: BillingCycle, planCycle: BillingCycle): boolean;
196
+ /** What a renewal job reads from a booking to decide whether to roll it on. */
197
+ interface BundlePeriodRollInput {
198
+ /**
199
+ * Null in two different situations, and the difference decides everything:
200
+ * a booking made before these columns existed, and one whose plan had no
201
+ * period to align to yet — a trial, or a subscription awaiting sales.
202
+ *
203
+ * `billingCycle` is what tells them apart. The booking route always writes
204
+ * it, so a row that has one but no window is the second case and is waiting
205
+ * for a window; a row with neither is the first and is billed with the plan.
206
+ */
207
+ currentPeriodEnd: Date | null;
208
+ /** Null only on a booking made before bundles had a rhythm of their own. */
209
+ billingCycle: BillingCycle | null;
210
+ canceledAt: Date | null;
211
+ canceledEffectiveAt: Date | null;
212
+ }
213
+ /** The plan the booking hangs off, as the same job reads it. */
214
+ interface BundlePlanContext {
215
+ billingCycle: BillingCycle;
216
+ billingAnchorDay: number | null;
217
+ /** When the plan ends, or null while it runs on. */
218
+ endsAt: Date | null;
219
+ /**
220
+ * The plan's own window, which a booking still waiting for one joins.
221
+ *
222
+ * Null while the plan has no paid period either — then there is nothing to
223
+ * align to and the booking keeps waiting.
224
+ */
225
+ currentPeriodStart?: Date | null;
226
+ currentPeriodEnd?: Date | null;
227
+ }
228
+ /** The window a roll opens. */
229
+ interface BundlePeriodWindow {
230
+ currentPeriodStart: Date;
231
+ currentPeriodEnd: Date;
232
+ }
233
+ /**
234
+ * The window a booking should hold now, or `null` for nothing to do.
235
+ *
236
+ * The bundle counterpart of `computeNextPeriod`, and the same division of
237
+ * labour: the platform decides, the consumer's cron reads, writes and audits.
238
+ * Without it the columns written at booking would never move again, and "every
239
+ * period after the first runs anchor to anchor" would be a claim nothing keeps.
240
+ *
241
+ * It answers two questions the job cannot tell apart from the outside, because
242
+ * both look like a booking whose window is not current:
243
+ *
244
+ * - **Opening the first window.** A bundle booked while its plan had no period —
245
+ * during a trial, or before sales finished — was stored without one, because
246
+ * there was nothing to align to. Once the plan has a paid period the booking
247
+ * joins it. Left undone, a monthly bundle on a yearly trial kept granting its
248
+ * features and never acquired a window to bill them in.
249
+ * - **Rolling the next one.** The ordinary case, anchor to anchor.
250
+ *
251
+ * It declines in four:
252
+ *
253
+ * - **A booking billed with the plan** — written before these columns existed,
254
+ * recognisable by having no rhythm of its own either. Giving it a window would
255
+ * start billing it a second time.
256
+ * - **The plan has no paid period yet**, so there is still nothing to align to.
257
+ * - **The period is still running.** The job's filter should not have offered it.
258
+ * - **The booking's own cancellation has landed.** A declared one changes
259
+ * nothing, exactly as for a subscription.
260
+ * - **The plan has ended, or ends before the new period could open.** This is
261
+ * the rule the alignment exists for: the bundle ends with the plan, without a
262
+ * cancellation of its own.
263
+ *
264
+ * Where the plan ends *inside* the new period — which only happens when someone
265
+ * ends it off-anchor — the window is cut back to the plan's end rather than
266
+ * allowed to outlive it. A bundle committed past its plan is the one state this
267
+ * whole arrangement is built to prevent.
268
+ */
269
+ declare function computeNextBundlePeriod(booking: BundlePeriodRollInput, plan: BundlePlanContext, now: Date): BundlePeriodWindow | null;
270
+
271
+ /**
272
+ * List price (net) for the billing cycle, including a plan-specific pricing
273
+ * override (`BundlePricingOverride` with `planId`).
274
+ *
275
+ * `null` means no price is maintained for that combination — not that the
276
+ * bundle is free. A published bundle always resolves *some* price, which the
277
+ * publish gate checks; what it cannot check is the combination, since that
278
+ * depends on the plan the tenant is on and the rhythm they are billed in.
279
+ */
280
+ declare function resolveBundlePriceNet(bundleVersion: BundleVersionRow, planKey: string, billingCycle: string): number | null;
281
+ /**
282
+ * The cycles a plan version is actually sold in.
283
+ *
284
+ * Derived from the prices it carries rather than from a list of cycles, so a
285
+ * plan priced monthly only is a monthly plan and nothing has to say so
286
+ * separately. A version with neither price is sold in no cycle at all — the
287
+ * plan's own publish gate refuses that, and here it simply asks nothing.
288
+ */
289
+ declare function cyclesSoldFor(planVersion: {
290
+ monthlyNet?: string | null;
291
+ yearlyNet?: string | null;
292
+ }): BillingCycle[];
46
293
 
47
294
  /** Generic shape of a PlanVersion snapshot for publish validation. */
48
295
  interface PublishablePlanVersion {
@@ -113,6 +360,16 @@ interface RenewalSubInput {
113
360
  pendingPlanVersionAccepted: boolean;
114
361
  /** `nonRegressive` from the referenced PlanVersion. */
115
362
  pendingPlanVersionNonRegressive: boolean;
363
+ /**
364
+ * The cancellation, read the same way `computeNextPeriod` reads it below.
365
+ *
366
+ * A version published before the customer cancelled still comes due
367
+ * afterwards, and rolling it forward rewrites the plan of a subscription
368
+ * whose term is over. Required, and required together, because a record
369
+ * that omits them answers "not cancelled" and goes ahead.
370
+ */
371
+ canceledAt: Date | null;
372
+ canceledEffectiveAt: Date | null;
116
373
  }
117
374
  /**
118
375
  * Decides what should happen to a subscription with a due pending version.
@@ -134,25 +391,66 @@ declare function clearPendingPlanVersionFields(): {
134
391
  };
135
392
  /** Input shape for `computeNextPeriod`. */
136
393
  interface PeriodRollInput {
394
+ /**
395
+ * The day of the month this subscription is billed on, or null on a row
396
+ * that predates the column. Null falls back to the day of the period end,
397
+ * which is the reading that drifts — so a consumer that stores the anchor
398
+ * gets the correct date and one that does not keeps today's behaviour.
399
+ */
400
+ billingAnchorDay?: number | null;
137
401
  /** Subscription.currentPeriodEnd. NULL → no period active → SKIP. */
138
402
  currentPeriodEnd: Date | null;
139
403
  billingCycle: BillingCycle;
404
+ /**
405
+ * When a cancellation was declared — and, on a row written before the two
406
+ * fields parted company, also when it lands.
407
+ *
408
+ * It used to stop the renewal on its own, which was right while it carried
409
+ * both meanings. Since they separated it is normally only "the customer
410
+ * said so": a subscription cancelled in month three of a year still runs,
411
+ * and stopping its renewal would end it nine months early.
412
+ *
413
+ * The exception is the row that predates the split. There `canceledAt`
414
+ * holds the period end the old code computed and `canceledEffectiveAt` is
415
+ * null, so reading only the second would roll a cancelled subscription into
416
+ * another paid term, and the next one, forever.
417
+ */
140
418
  canceledAt: Date | null;
419
+ /**
420
+ * When that cancellation lands. This is what stops the renewal.
421
+ *
422
+ * Null while none was declared. A period may still roll onto it: with a
423
+ * notice period configured, a late cancellation lands at the end of the
424
+ * FOLLOWING period, and that period has to exist to end.
425
+ */
426
+ canceledEffectiveAt: Date | null;
141
427
  }
142
428
  /** Result: the next period window or `null` (skip). */
143
429
  interface NextPeriodWindow {
144
430
  currentPeriodStart: Date;
145
431
  currentPeriodEnd: Date;
432
+ /**
433
+ * The renewed commitment, which is the period itself.
434
+ *
435
+ * The minimum term IS the chosen billing period (rule a), it starts at
436
+ * activation (rule c) and it renews with the period unless a cancellation
437
+ * was declared first (rule d). Written on every roll so nothing has to
438
+ * reconstruct it later from a start date and a cycle.
439
+ */
440
+ minimumTermUntil: Date;
146
441
  }
147
442
  /**
148
443
  * Computes the next period window. `null` means: no action
149
444
  * (either the period hasn't been reached yet, canceled, or NULL period).
150
445
  *
151
446
  * Logic (spec: SUPERADMIN_PLANS_DASHBOARD_TODO §2.2):
152
- * - If `canceledAt` is set → SKIP.
447
+ * - If a cancellation has LANDED → SKIP. A declared one has not, unless the
448
+ * row predates the split and carries its effective date in `canceledAt`.
153
449
  * - If `currentPeriodEnd === null` → SKIP (trial / PENDING_SALES).
154
450
  * - If `currentPeriodEnd > now` → SKIP (period still active).
155
- * - Otherwise → start := old `currentPeriodEnd`, end := periodEndAfter(start).
451
+ * - If a cancellation has LANDED → SKIP. A declared one has not.
452
+ * - Otherwise → start := old `currentPeriodEnd`, end := periodEndAfter(start),
453
+ * and the minimum term renews with it.
156
454
  */
157
455
  declare function computeNextPeriod(sub: PeriodRollInput, now: Date): NextPeriodWindow | null;
158
456
 
@@ -230,10 +528,9 @@ declare const UPSELL_OFFER_CURRENCY_TOKEN: unique symbol;
230
528
 
231
529
  declare class CatalogBundleUpsellResolver implements UpsellOfferResolver {
232
530
  private readonly bundleRepo;
233
- private readonly projectKey;
234
531
  private readonly catalogEntryRepo;
235
532
  private readonly currency;
236
- constructor(bundleRepo: BundleRepository, projectKey: string, catalogEntryRepo?: CatalogEntryRepository | null, currency?: string | null);
533
+ constructor(bundleRepo: BundleRepository, catalogEntryRepo?: CatalogEntryRepository | null, currency?: string | null);
237
534
  resolveOffers(featureKeys: string[], tenantId: string): Promise<UpsellOffer[]>;
238
535
  /**
239
536
  * Uncovered dependencies of the missing features (#35) — feed into the
@@ -251,66 +548,18 @@ declare class LimitExceededFilter extends BaseExceptionFilter {
251
548
  catch(exception: unknown, host: ArgumentsHost): void;
252
549
  }
253
550
 
551
+ /** The settings of `config/saas.yaml` — a `PlanCatalogSettings`, fixed while the process runs. */
552
+ declare const PLAN_CATALOG_SETTINGS_TOKEN: unique symbol;
553
+ /** A `PlanCatalogSource`: the plans and features as they stand when an operation asks. */
554
+ declare const PLAN_CATALOG_SOURCE_TOKEN: unique symbol;
555
+ declare const PLAN_CATALOG_READ_SINK_TOKEN: unique symbol;
254
556
  /**
255
- * Schema validation error bundling all Ajv errors — one call returns
256
- * the full list, no round-trip editing needed.
257
- */
258
- interface AjvErrorLike {
259
- instancePath?: string;
260
- message?: string;
261
- schemaPath?: string;
262
- }
263
- declare class PlanCatalogValidationError extends Error {
264
- readonly source: string;
265
- readonly errors: AjvErrorLike[];
266
- constructor(source: string, errors: AjvErrorLike[]);
267
- }
268
- interface LoadPlanCatalogOptions {
269
- /**
270
- * Absolute path or relative path (resolved against CWD).
271
- */
272
- path: string;
273
- /**
274
- * Optional: additional cross-field validations that the JSON schema
275
- * cannot cover. Default: enable all (see validateConsistency).
276
- */
277
- crossFieldChecks?: boolean;
278
- }
279
- /**
280
- * Loads + validates a saas.yaml file.
281
- *
282
- * Throws `PlanCatalogValidationError` on schema violations or
283
- * cross-field violations. Throws `Error` on IO/YAML parse errors.
284
- */
285
- declare function loadPlanCatalogFromFile(opts: LoadPlanCatalogOptions): PlanCatalog;
286
- /**
287
- * Variant for tests / in-memory loading: takes YAML content as a string,
288
- * `source` is only for error logging.
557
+ * Every settings block of `config/saas.yaml` — app identity, currency, VAT rate,
558
+ * tenant billing and the rest — beside the read sink the plans and features
559
+ * come from. The settings are handed on whole, so a block the schema gains
560
+ * reaches `PLAN_CATALOG_SETTINGS_TOKEN` without this module learning its name.
289
561
  */
290
- declare function loadPlanCatalogFromString(yamlContent: string, opts: {
291
- source: string;
292
- crossFieldChecks?: boolean;
293
- }): PlanCatalog;
294
-
295
- declare const PLAN_CATALOG_TOKEN: unique symbol;
296
- declare const PLAN_CATALOG_READ_SINK_TOKEN: unique symbol;
297
- interface PlanCatalogModuleOptions {
298
- /** Build-time identity of the app. */
299
- projectKey: string;
300
- /**
301
- * App-identity block (branding + version) from `config/saas.yaml#app`.
302
- * Flows into `PLAN_CATALOG_TOKEN.app` and from there into the
303
- * AdminPublicBoot endpoint + the AdminManifestConfig.
304
- */
305
- app?: PlanCatalog['app'];
306
- currency: string;
307
- vatRate: number;
308
- /**
309
- * App-wide marketing configuration — including the
310
- * `availableLocales` pool. Flows into `PLAN_CATALOG_TOKEN.marketing`
311
- * and from there into the admin manifest (`project.availableLocales`).
312
- */
313
- marketing?: PlanCatalog['marketing'];
562
+ interface PlanCatalogModuleOptions extends PlanCatalogSettings {
314
563
  /** App-specific adapter for DB reads. */
315
564
  sink: ProviderSpec<PlanCatalogReadSink>;
316
565
  /** Modules that must be visible in the DI scope (analogous to CatalogModule). */
@@ -330,15 +579,12 @@ declare class PlanCatalogModule {
330
579
  }): DynamicModule;
331
580
  }
332
581
 
333
- interface PlanCatalogBuildSettings {
334
- projectKey: string;
335
- /** App identity (branding + version) from `config/saas.yaml#app`. Optional. */
336
- app?: PlanCatalog['app'];
337
- currency: string;
338
- vatRate: number;
339
- /** App-wide marketing configuration. Optional. */
340
- marketing?: PlanCatalog['marketing'];
341
- }
582
+ /**
583
+ * The settings a database catalogue runs on. The database carries plans and
584
+ * features, never the settings, so they can only come from the file — every
585
+ * block of it, which is why this is the settings type rather than a list.
586
+ */
587
+ type PlanCatalogBuildSettings = PlanCatalogSettings;
342
588
  declare function buildPlanCatalogFromSnapshot(settings: PlanCatalogBuildSettings, snapshot: PlanCatalogReadSnapshot): PlanCatalog;
343
589
 
344
590
  declare const PLAN_CATALOG_IMPORT_SINK_TOKEN: unique symbol;
@@ -362,8 +608,10 @@ declare class PlanCatalogImporterService {
362
608
  interface PlanCatalogImporterControllerConfig {
363
609
  /**
364
610
  * Class-level guards for `/admin/billing/plan-catalog/import`. Required
365
- * when `controller` is set — pass `[]` explicitly for auth-free
366
- * (only sensible in tests).
611
+ * when `controller` is set. The import publishes the plans it creates, so
612
+ * the handler requires the second factor whatever this list holds, and
613
+ * `MfaService` has to be resolvable through `AdminModule`; with `[]`
614
+ * nothing establishes a caller and the import is refused.
367
615
  */
368
616
  guards: Array<Type<CanActivate>>;
369
617
  }
@@ -397,7 +645,7 @@ declare function getPlanOrThrow(catalog: PlanCatalog, planId: PlanId): PlanDef;
397
645
  /**
398
646
  * Returns all marketed plans (`marketed: true` or undefined). Order as in the
399
647
  * catalog. ENTERPRISE and other `marketed: false` plans are NOT included —
400
- * these can only be activated via `ahp paket apply` / special contract and do
648
+ * these are activated by a catalogue apply or a special contract, and do
401
649
  * not belong in self-service onboarding lists.
402
650
  */
403
651
  declare function getMarketedPlans(catalog: PlanCatalog): PlanDef[];
@@ -409,10 +657,31 @@ declare function getMarketedPlans(catalog: PlanCatalog): PlanDef[];
409
657
  * - the plan has no price for the cycle (`monthlyNet`/`yearlyNet === null`)
410
658
  */
411
659
  declare function getPlanPriceNet(catalog: PlanCatalog, planId: PlanId, cycle: BillingCycle): number | null;
660
+ /** The same rule for a plan already in hand, whichever version it describes. */
661
+ declare function listPriceNet(plan: PlanDef, cycle: BillingCycle): number | null;
662
+ /**
663
+ * Whether a plan on the list carries no price for a cycle, and so is not sold
664
+ * in it: a plan without a yearly price is a monthly plan, one without either is
665
+ * sold on request. A plan that is not marketed is sold under a special
666
+ * contract, whose price the catalogue does not hold, so this is never true of
667
+ * it.
668
+ */
669
+ declare function isPlanNotSoldInCycle(plan: PlanDef, cycle: BillingCycle): boolean;
412
670
  /**
413
- * Gross list price from the catalog (net * (1 + vatRate/100)).
671
+ * The refusal for a plan that is not sold in a cycle — the same body for the
672
+ * plan change's blocker and for the contract, which would otherwise record the
673
+ * plan at 0.00.
674
+ */
675
+ declare function planNotSoldInCycle(plan: PlanDef, cycle: BillingCycle): {
676
+ code: string;
677
+ message: string;
678
+ params: Record<string, string>;
679
+ };
680
+ /**
681
+ * Gross list price from the catalog, through `grossFromNet`.
414
682
  * `null` with the same rules as `getPlanPriceNet`. `vatRate` is optional;
415
- * default: `catalog.vatRate`.
683
+ * default: `catalog.vatRate`. A rate that is not a percentage is refused, as it
684
+ * is wherever a rate is stated.
416
685
  */
417
686
  declare function getPlanPriceGross(catalog: PlanCatalog, planId: PlanId, cycle: BillingCycle, vatRate?: number): number | null;
418
687
  /**
@@ -492,13 +761,12 @@ interface PublicBundleEntry {
492
761
  marketing?: MarketingFields;
493
762
  }
494
763
  declare class PublicCatalogController {
495
- private readonly planCatalog;
764
+ private readonly planCatalogs;
496
765
  private readonly featureRegistry;
497
- private readonly projectKey;
498
766
  private readonly marketingRepo;
499
767
  private readonly bundleRepo;
500
768
  private readonly catalogEntryRepo;
501
- constructor(planCatalog: PlanCatalog, featureRegistry: FeatureUiRegistry, projectKey?: string | null, marketingRepo?: MarketingProjectionRepository | null, bundleRepo?: BundleRepository | null, catalogEntryRepo?: CatalogEntryRepository | null);
769
+ constructor(planCatalogs: PlanCatalogSource, featureRegistry: FeatureUiRegistry, marketingRepo?: MarketingProjectionRepository | null, bundleRepo?: BundleRepository | null, catalogEntryRepo?: CatalogEntryRepository | null);
502
770
  listPlans(lang?: string, localeParam?: string): Promise<PlanResponseEntry[]>;
503
771
  listFeatureRegistry(): Promise<FeatureUiRegistry>;
504
772
  /**
@@ -521,12 +789,6 @@ declare class PublicCatalogController {
521
789
  interface PublicCatalogModuleOptions {
522
790
  /** Required: consumer-specific FeatureUiRegistry. */
523
791
  featureUiRegistry: FeatureUiRegistry;
524
- /**
525
- * — app identity (e.g. "clubapp"). Used
526
- * for marketing lookups + bundle filters.
527
- * Optional; if omitted, the new endpoints return empty lists.
528
- */
529
- projectKey?: string;
530
792
  /**
531
793
  * Optional. When set, `/billing/bundles` is active.
532
794
  */
@@ -537,9 +799,9 @@ interface PublicCatalogModuleOptions {
537
799
  */
538
800
  marketingRepository?: ProviderSpec<MarketingProjectionRepository>;
539
801
  /**
540
- * Optional (#13). When set (+ projectKey), `/billing/feature-registry`
541
- * overlays the editable `FeatureCatalogEntry.icon` from the DB over the
542
- * static registry.
802
+ * Optional (#13). When set, `/billing/feature-registry` overlays the
803
+ * editable `FeatureCatalogEntry.icon` from the DB over the static
804
+ * registry.
543
805
  */
544
806
  catalogEntryRepository?: ProviderSpec<CatalogEntryRepository>;
545
807
  /**
@@ -553,22 +815,101 @@ declare class PublicCatalogModule {
553
815
  static forRoot(options: PublicCatalogModuleOptions): DynamicModule;
554
816
  }
555
817
 
556
- declare const PUBLIC_CATALOG_PROJECT_KEY_TOKEN: unique symbol;
557
818
  declare const PUBLIC_CATALOG_BUNDLE_REPOSITORY_TOKEN: unique symbol;
558
819
  declare const PUBLIC_CATALOG_MARKETING_REPOSITORY_TOKEN: unique symbol;
559
820
 
821
+ /**
822
+ * The billing permission: who of a tenant's users may see and change what the
823
+ * subscriber pays with — and, as they arrive, its invoices and its account.
824
+ * The plan, the usage and a change's preview are not behind it.
825
+ *
826
+ * The application decides who holds it by passing guards
827
+ * (`payments.billingPermissionGuards`), so a role such as accounting can hold
828
+ * it without being the tenant's administrator. Without guards the tenant's
829
+ * administrator holds it. Runs after the tenant's authentication guards, so a
830
+ * user is on the request.
831
+ */
832
+ declare class BillingPermissionGuard implements CanActivate {
833
+ private readonly guards;
834
+ constructor(guards?: AuthGuardList | null);
835
+ canActivate(context: ExecutionContext): Promise<boolean>;
836
+ private holdsPermission;
837
+ }
838
+
560
839
  declare class SubscriptionContractFreezeService implements ContractFreezePort {
561
- private readonly catalog;
840
+ private readonly catalogs;
562
841
  private readonly entitlements;
563
842
  private readonly contracts;
564
- private readonly projectKey;
565
843
  private readonly source;
566
- constructor(catalog: PlanCatalog, entitlements: EntitlementService, contracts: SubscriptionContractService, projectKey: string, source: ContractFreezeSourcePort);
567
- freezeOnPlanChange(tenantId: string, newPlan: string, billingCycle: BillingCycle, effectiveFrom: Date): Promise<void>;
844
+ constructor(catalogs: PlanCatalogSource, entitlements: EntitlementService, contracts: SubscriptionContractService, source: ContractFreezeSourcePort, writes?: TenantSubscriptionWritePort | null);
845
+ assertPartyFor(tenantId: string): Promise<void>;
846
+ endOnCancellation(tenantId: string, effectiveAt: Date): Promise<void>;
847
+ freezeOnPlanChange(tenantId: string, newPlan: string, billingCycle: BillingCycle, effectiveFrom: Date, endsAt?: Date | null): Promise<void>;
568
848
  }
569
849
 
570
850
  declare function computeCarriedTrialEndsAt(currentTrialDays: number, newTrialDays: number, currentTrialEndsAt: Date, now: Date): Date;
571
851
 
852
+ declare class AddSubscriptionBundleDto {
853
+ bundleVersionId: string;
854
+ /**
855
+ * Optional — override for the minimum term (months). `0` = no
856
+ * minimum term. Default comes from the `SubscriptionBundleConfig`
857
+ * (platform = no commitment).
858
+ */
859
+ minimumTermMonths?: number;
860
+ /**
861
+ * The rhythm to bill this bundle in. Defaults to the plan's.
862
+ *
863
+ * A bundle may run in a shorter rhythm than its plan — monthly beside a
864
+ * yearly plan is the interesting case — but never a longer one. A yearly
865
+ * bundle on a monthly plan is refused rather than modelled: it has no
866
+ * boundary to meet, and every one of the plan's twelve period ends is a
867
+ * moment the plan could stop and leave the bundle committed with nothing to
868
+ * grant.
869
+ */
870
+ billingCycle?: 'MONTHLY' | 'YEARLY';
871
+ }
872
+ /**
873
+ * Body of `POST /billing/subscription-bundles/preview` (#37). Exactly one
874
+ * of `bundleVersionId` (add preview) or `subscriptionBundleId`
875
+ * (cancel preview) — the controller enforces this.
876
+ */
877
+ declare class PreviewSubscriptionBundleDto {
878
+ /** Add preview: BundleVersion to be booked. */
879
+ bundleVersionId?: string;
880
+ /** Cancel preview: existing Bundle booking. */
881
+ subscriptionBundleId?: string;
882
+ /** Add preview only — override analogous to `AddSubscriptionBundleDto`. */
883
+ minimumTermMonths?: number;
884
+ /**
885
+ * Add preview only — the rhythm to quote, analogous to
886
+ * `AddSubscriptionBundleDto`. Defaults to the plan's.
887
+ *
888
+ * The booking has taken this since bundles gained a rhythm of their own.
889
+ * The preview not taking it meant a tenant asking for a monthly bundle on a
890
+ * yearly plan was quoted the yearly one and then charged the monthly one —
891
+ * a preview describing a different contract from the one written, which is
892
+ * the one thing a preview may never do.
893
+ */
894
+ billingCycle?: 'MONTHLY' | 'YEARLY';
895
+ }
896
+ declare class CancelSubscriptionBundleDto {
897
+ /**
898
+ * Optional — default = `new Date()` server-side. Format: ISO-8601
899
+ * (`YYYY-MM-DD` or full timestamp). Usually not set by the tenant
900
+ * self-service.
901
+ */
902
+ canceledAt?: string;
903
+ }
904
+ /** Which bundles the store is showing, so their prices can be resolved. */
905
+ declare class BundlePriceLookupDto {
906
+ /**
907
+ * Capped rather than unbounded: the caller names what it is displaying, and
908
+ * a page shows a catalogue, not a database. Each id costs a lookup.
909
+ */
910
+ bundleVersionIds: string[];
911
+ }
912
+
572
913
  /**
573
914
  * Builds the `ConfiguratorCatalog` (for onboarding step 3) from the
574
915
  * live `plan_versions` (SuperAdmin defines plans + prices) plus the
@@ -586,14 +927,13 @@ declare class ConfiguratorCatalogBuilder {
586
927
  }
587
928
 
588
929
  declare const SUBSCRIPTION_BUNDLE_REPOSITORY_TOKEN: unique symbol;
589
- /** Optional config token; default = 12 months minimum term. */
930
+ /** Optional config token; without it a booking commits the tenant to nothing. */
590
931
  declare const SUBSCRIPTION_BUNDLE_CONFIG_TOKEN: unique symbol;
591
932
 
592
933
  /**
593
934
  * Factory analogous to `buildBundlesController` etc. — the consumer can pass
594
- * additional guards (e.g. RoleGuard) in addition to the platform default
595
- * `ComposedTenantAuthGuard`.
935
+ * additional guards (e.g. RoleGuard) in addition to the platform defaults.
596
936
  */
597
937
  declare function buildTenantSubscriptionBundlesController(extraGuards?: Array<Type<CanActivate>>): Type;
598
938
 
599
- export { type AjvErrorLike, BILLING_FEATURE_UI_REGISTRY_TOKEN, CatalogBundleUpsellResolver, ConfiguratorCatalogBuilder, ContractFreezePort, ContractFreezeSourcePort, FEATURE_GUARD_CONFIG_TOKEN, FEATURE_GUARD_MARKER, FeatureGuard, type FeatureGuardConfig, LimitExceededFilter, type LoadPlanCatalogOptions, type MarketingFields, type NextPeriodWindow, PLAN_CATALOG_IMPORT_SINK_TOKEN, PLAN_CATALOG_READ_SINK_TOKEN, PLAN_CATALOG_TOKEN, PUBLIC_CATALOG_BUNDLE_REPOSITORY_TOKEN, PUBLIC_CATALOG_MARKETING_REPOSITORY_TOKEN, PUBLIC_CATALOG_PROJECT_KEY_TOKEN, type PeriodRollInput, type PlanCatalogBuildSettings, PlanCatalogImportDto, type PlanCatalogImporterControllerConfig, PlanCatalogImporterModule, type PlanCatalogImporterModuleOptions, PlanCatalogImporterService, PlanCatalogModule, type PlanCatalogModuleOptions, PlanCatalogValidationError, type PlanResponseEntry, type PublicBundleEntry, PublicCatalogController, PublicCatalogModule, type PublicCatalogModuleOptions, PublishValidationError, type PublishablePlanVersion, REQUIRE_FEATURE_KEY, type RenewalDecision, type RenewalSubInput, RequireFeature, SUBSCRIPTION_BUNDLE_CONFIG_TOKEN, SUBSCRIPTION_BUNDLE_REPOSITORY_TOKEN, SubscriptionContractFreezeService, UPSELL_OFFER_CURRENCY_TOKEN, UPSELL_OFFER_RESOLVER_TOKEN, assertBaseVersionFresh, assertChangeNote, assertDraftPublishable, assertOptimisticLockHeld, buildPlanCatalogFromSnapshot, buildPlanCatalogImporterController, buildTenantSubscriptionBundlesController, clearPendingPlanVersionFields, computeCarriedTrialEndsAt, computeNextPeriod, decideRenewal, findPlan, getActiveFeatureKeys, getMarketedPlans, getPlanOrThrow, getPlanPriceGross, getPlanPriceNet, getPlanQuota, initialPeriodWindow, isFeatureInPlan, isFeaturePlannedOnly, loadPlanCatalogFromFile, loadPlanCatalogFromString, periodEndAfter, periodEndWithMinLead };
939
+ export { AddSubscriptionBundleDto, AuthGuardList, BILLING_FEATURE_UI_REGISTRY_TOKEN, BillingPermissionGuard, type BundleFirstPeriodInput, type BundlePeriodRollInput, type BundlePeriodWindow, type BundlePlanContext, BundlePriceLookupDto, CancelSubscriptionBundleDto, CatalogBundleUpsellResolver, ConfiguratorCatalogBuilder, ContractFreezePort, ContractFreezeSourcePort, DEFAULT_BUNDLE_MINIMUM_TERM_MONTHS, FEATURE_GUARD_CONFIG_TOKEN, FEATURE_GUARD_MARKER, FeatureGuard, type FeatureGuardConfig, LimitExceededFilter, type MarketingFields, type NextPeriodWindow, PLAN_CATALOG_IMPORT_SINK_TOKEN, PLAN_CATALOG_READ_SINK_TOKEN, PLAN_CATALOG_SETTINGS_TOKEN, PLAN_CATALOG_SOURCE_TOKEN, PUBLIC_CATALOG_BUNDLE_REPOSITORY_TOKEN, PUBLIC_CATALOG_MARKETING_REPOSITORY_TOKEN, type PeriodRollInput, type PlanAnchorSource, type PlanCatalogBuildSettings, PlanCatalogImportDto, type PlanCatalogImporterControllerConfig, PlanCatalogImporterModule, type PlanCatalogImporterModuleOptions, PlanCatalogImporterService, PlanCatalogModule, type PlanCatalogModuleOptions, PlanCatalogSource, type PlanResponseEntry, PreviewSubscriptionBundleDto, type PublicBundleEntry, PublicCatalogController, PublicCatalogModule, type PublicCatalogModuleOptions, PublishValidationError, type PublishablePlanVersion, REQUIRE_FEATURE_KEY, type RenewalDecision, type RenewalSubInput, RequireFeature, SUBSCRIPTION_BUNDLE_CONFIG_TOKEN, SUBSCRIPTION_BUNDLE_REPOSITORY_TOKEN, SubscriptionContractFreezeService, UPSELL_OFFER_CURRENCY_TOKEN, UPSELL_OFFER_RESOLVER_TOKEN, advanceOneCycle, assertBaseVersionFresh, assertChangeNote, assertDraftPublishable, assertOptimisticLockHeld, billingAnchorDay, buildPlanCatalogFromSnapshot, buildPlanCatalogImporterController, buildTenantSubscriptionBundlesController, bundleCycleFitsPlan, bundleFirstPeriodEnd, bundleFirstPeriodStart, bundleNextPeriodEnd, clearPendingPlanVersionFields, computeCarriedTrialEndsAt, computeNextBundlePeriod, computeNextPeriod, cyclesSoldFor, decideRenewal, findPlan, getActiveFeatureKeys, getMarketedPlans, getPlanOrThrow, getPlanPriceGross, getPlanPriceNet, getPlanQuota, initialPeriodWindow, isFeatureInPlan, isFeaturePlannedOnly, isPlanNotSoldInCycle, listPriceNet, periodEndAfter, periodEndWithMinLead, planNotSoldInCycle, resolveBundlePriceNet, resolvePlanAnchorDay, retreatOneCycle };