@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
@@ -0,0 +1,946 @@
1
+ import {
2
+ PromoCodesService,
3
+ computeDiscountGross
4
+ } from "./chunk-3RLWML2S.js";
5
+ import {
6
+ SubscriptionContractService,
7
+ appendImplicitDiscountLineItem
8
+ } from "./chunk-NDLC5GYK.js";
9
+ import {
10
+ resolveBundlePriceNet
11
+ } from "./chunk-KPEMTBOP.js";
12
+ import {
13
+ grossFromNet,
14
+ netFromGross,
15
+ round2
16
+ } from "./chunk-6PJE7EWO.js";
17
+ import {
18
+ BUNDLE_REPOSITORY_TOKEN,
19
+ CATALOG_ENTRY_REPOSITORY_TOKEN,
20
+ PLAN_REPOSITORY_TOKEN,
21
+ PROMOTION_REPOSITORY_TOKEN
22
+ } from "./chunk-3QVGA6JX.js";
23
+ import {
24
+ SubscriberService,
25
+ settleNewSubscriberDetails
26
+ } from "./chunk-LF6J4YYN.js";
27
+ import {
28
+ PLAN_CATALOG_SETTINGS_TOKEN
29
+ } from "./chunk-FEQZCWUL.js";
30
+ import {
31
+ codedError
32
+ } from "./chunk-J7NJRK3K.js";
33
+ import {
34
+ __name
35
+ } from "./chunk-SHUYVCID.js";
36
+
37
+ // src/checkout-offer/checkout-offer-pricing.ts
38
+ import { Inject, Injectable, Optional, UnprocessableEntityException } from "@nestjs/common";
39
+ import { CONTRACT_ERROR_CODES, promotionOnPrice } from "@saasicat/core";
40
+
41
+ // src/checkout-offer/bundle-version-bookable.ts
42
+ import { startOfUtcDay } from "@saasicat/core";
43
+ function bundleVersionNotBookableReason(version, nowMs) {
44
+ if (version.publishedAt === null) return "not_published";
45
+ if (version.supersededAt !== null) return "superseded";
46
+ if (dateIsAfter(version.validFrom, nowMs)) return "not_yet_valid";
47
+ if (isValidUntilExpired(version.validUntil, nowMs)) return "expired";
48
+ return null;
49
+ }
50
+ __name(bundleVersionNotBookableReason, "bundleVersionNotBookableReason");
51
+ function dateIsAfter(value, nowMs) {
52
+ if (!value) return false;
53
+ const time = new Date(value).getTime();
54
+ return Number.isNaN(time) || time > nowMs;
55
+ }
56
+ __name(dateIsAfter, "dateIsAfter");
57
+ function isValidUntilExpired(value, nowMs) {
58
+ if (!value) return false;
59
+ const validUntil = new Date(value).getTime();
60
+ if (Number.isNaN(validUntil)) return true;
61
+ return validUntil < startOfUtcDay(new Date(nowMs)).getTime();
62
+ }
63
+ __name(isValidUntilExpired, "isValidUntilExpired");
64
+
65
+ // src/checkout-offer/checkout-offer-pricing.ts
66
+ function _ts_decorate(decorators, target, key, desc) {
67
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
68
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
69
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
70
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
71
+ }
72
+ __name(_ts_decorate, "_ts_decorate");
73
+ function _ts_metadata(k, v) {
74
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
75
+ }
76
+ __name(_ts_metadata, "_ts_metadata");
77
+ function _ts_param(paramIndex, decorator) {
78
+ return function(target, key) {
79
+ decorator(target, key, paramIndex);
80
+ };
81
+ }
82
+ __name(_ts_param, "_ts_param");
83
+ var DEFAULT_LOCALE = "de";
84
+ var CheckoutOfferPricing = class {
85
+ static {
86
+ __name(this, "CheckoutOfferPricing");
87
+ }
88
+ settings;
89
+ plans;
90
+ bundles;
91
+ promotions;
92
+ promoCodes;
93
+ constructor(settings, plans, bundles = null, promotions = null, promoCodes = null) {
94
+ this.settings = settings;
95
+ this.plans = plans;
96
+ this.bundles = bundles;
97
+ this.promotions = promotions;
98
+ this.promoCodes = promoCodes;
99
+ }
100
+ /** Prices a selection against the plan version on sale now. */
101
+ async price(input, asOf = /* @__PURE__ */ new Date()) {
102
+ const planVersion = await this.planVersionOnSale(input.planKey, input.billingCycle, asOf);
103
+ const bundleVersions = await this.bundleVersionsFor(input, asOf, true);
104
+ return this.compute(input, planVersion, bundleVersions, asOf);
105
+ }
106
+ /**
107
+ * Throws unless the offer's stored amounts are what this pricing makes of
108
+ * its own selection: the plan and bundle versions it froze, and the
109
+ * promotions as they stood when it was last priced.
110
+ */
111
+ async assertPricedByCatalogue(offer) {
112
+ const notCurrent = /* @__PURE__ */ __name(() => new UnprocessableEntityException({
113
+ code: CONTRACT_ERROR_CODES.CHECKOUT_OFFER_PRICE_NOT_CURRENT,
114
+ message: `The prices of checkout offer '${offer.id}' no longer match the catalogue; create a new offer.`,
115
+ params: {
116
+ offerId: offer.id
117
+ }
118
+ }), "notCurrent");
119
+ if (!offer.planVersionId) throw notCurrent();
120
+ const planVersion = await this.plans.findVersionById?.(offer.planVersionId);
121
+ if (!planVersion || planVersion.planId !== offer.planKey) throw notCurrent();
122
+ const input = {
123
+ planKey: offer.planKey,
124
+ billingCycle: offer.billingCycle,
125
+ bundleVersionIds: offer.bundleVersionIds ?? [],
126
+ promoCode: offer.promoCode,
127
+ locale: offer.locale,
128
+ checkoutOfferId: offer.id,
129
+ concluding: true
130
+ };
131
+ const pricedAt = new Date(offer.updatedAt);
132
+ let repriced;
133
+ try {
134
+ this.assertPlanPriced(planVersion, input.billingCycle);
135
+ const bundleVersions = await this.bundleVersionsFor(input, pricedAt, false);
136
+ repriced = await this.compute(input, planVersion, bundleVersions, pricedAt);
137
+ } catch (error) {
138
+ if (refusesThePromoCode(error)) throw error;
139
+ if (error instanceof UnprocessableEntityException) throw notCurrent();
140
+ throw error;
141
+ }
142
+ if (moneyOf(repriced) !== moneyOf(offer)) throw notCurrent();
143
+ }
144
+ async compute(input, planVersion, bundleVersions, asOf) {
145
+ const vatRate = this.settings.vatRate;
146
+ const promotions = this.promotions ? await this.promotions.list() : [];
147
+ const plan = await this.plans.findByKey(input.planKey);
148
+ const planLine = this.pricePlan(input, planVersion, plan?.label, promotions, asOf);
149
+ const bundleLines = bundleVersions.map((version) => this.priceBundle(input, version, promotions, asOf));
150
+ const promotionSnapshots = [
151
+ planLine,
152
+ ...bundleLines
153
+ ].map((priced) => priced.promotion).filter((promotion) => promotion !== null);
154
+ const planNet = planLine.line.priceNet;
155
+ const bundlesNet = round2(bundleLines.reduce((sum, priced) => sum + priced.line.priceNet, 0));
156
+ const regularNet = round2(planNet + bundlesNet);
157
+ const promotionDiscount = round2(promotionSnapshots.reduce((sum, promotion) => sum + promotion.resolvedAmountNet, 0));
158
+ const planNetAfterPromotion = round2(planNet - (planLine.promotion?.resolvedAmountNet ?? 0));
159
+ const promoCodeSnapshot = await this.pricePromoCode(input, planNetAfterPromotion, vatRate);
160
+ const effectiveNet = Math.max(0, round2(regularNet - promotionDiscount - (promoCodeSnapshot?.resolvedAmountNet ?? 0)));
161
+ const priceBreakdown = {
162
+ currency: this.settings.currency,
163
+ billingCycle: input.billingCycle,
164
+ planNet,
165
+ bundlesNet,
166
+ regularNet,
167
+ effectiveNet,
168
+ vatRate,
169
+ effectiveGross: grossFromNet(effectiveNet, vatRate)
170
+ };
171
+ const lineItems = appendImplicitDiscountLineItem({
172
+ billingCycle: input.billingCycle,
173
+ priceBreakdown,
174
+ lineItems: [
175
+ planLine.line,
176
+ ...bundleLines.map((priced) => priced.line)
177
+ ],
178
+ promotionSnapshots,
179
+ promoCodeSnapshot
180
+ });
181
+ return {
182
+ planVersionId: planVersion.id,
183
+ bundles: bundleVersions.map((version) => version.bundleKey),
184
+ bundleVersionIds: bundleVersions.map((version) => version.id),
185
+ promotionId: planLine.promotion?.id ?? null,
186
+ promoCode: promoCodeSnapshot?.code ?? null,
187
+ priceBreakdown,
188
+ lineItems,
189
+ promotionSnapshots,
190
+ promoCodeSnapshot
191
+ };
192
+ }
193
+ /** The plan version on sale for new bookings, as the public catalogue resolves it. */
194
+ async planVersionOnSale(planKey, billingCycle, asOf) {
195
+ const version = await this.plans.findActivePlanVersion?.(planKey, asOf) ?? (this.plans.findActivePlanVersion ? null : await this.plans.findLatestLivePlanVersion?.(planKey));
196
+ if (!version) throw planNotOffered(planKey, billingCycle);
197
+ this.assertPlanPriced(version, billingCycle);
198
+ return version;
199
+ }
200
+ /** A plan without a list price is not sold at one (`SC-PRIC-011`). */
201
+ assertPlanPriced(version, billingCycle) {
202
+ if (version.marketed === false || priceOf(version, billingCycle) === null) {
203
+ throw planNotOffered(version.planId, billingCycle);
204
+ }
205
+ }
206
+ async bundleVersionsFor(input, asOf, checkBookable) {
207
+ const ids = input.bundleVersionIds;
208
+ if (ids.length === 0) return [];
209
+ const out = [];
210
+ for (const [index, bundleVersionId] of ids.entries()) {
211
+ if (ids.indexOf(bundleVersionId) !== index) {
212
+ throw bundleNotOffered(bundleVersionId, "duplicate");
213
+ }
214
+ const version = this.bundles ? await this.bundles.findVersionById(bundleVersionId) : null;
215
+ if (!version) throw bundleNotOffered(bundleVersionId, "missing");
216
+ if (checkBookable) {
217
+ const notBookable = bundleVersionNotBookableReason(version, asOf.getTime());
218
+ if (notBookable) throw bundleNotOffered(bundleVersionId, notBookable);
219
+ if (!version.marketed) throw bundleNotOffered(bundleVersionId, "not_marketed");
220
+ }
221
+ const planKeys = version.compatibility?.planIds ?? [];
222
+ if (planKeys.length > 0 && !planKeys.includes(input.planKey)) {
223
+ throw bundleNotOffered(bundleVersionId, "incompatible_with_plan");
224
+ }
225
+ if (resolveBundlePriceNet(version, input.planKey, wireCycle(input.billingCycle)) === null) {
226
+ throw bundleNotOffered(bundleVersionId, "not_priced");
227
+ }
228
+ out.push(version);
229
+ }
230
+ return out;
231
+ }
232
+ pricePlan(input, version, label, promotions, asOf) {
233
+ const priceNet = priceOf(version, input.billingCycle);
234
+ return {
235
+ line: this.line({
236
+ kind: "plan",
237
+ sourceKey: input.planKey,
238
+ sourceVersionId: version.id,
239
+ title: label ?? input.planKey,
240
+ priceNet,
241
+ billingCycle: input.billingCycle,
242
+ features: version.features ?? [],
243
+ quotas: version.quotas ?? {}
244
+ }),
245
+ promotion: promotionFor(promotions, input.planKey, "PLAN", input, priceNet, asOf)
246
+ };
247
+ }
248
+ priceBundle(input, version, promotions, asOf) {
249
+ const priceNet = resolveBundlePriceNet(version, input.planKey, wireCycle(input.billingCycle));
250
+ return {
251
+ line: this.line({
252
+ kind: "bundle",
253
+ sourceKey: version.bundleKey,
254
+ sourceVersionId: version.id,
255
+ title: version.label,
256
+ priceNet,
257
+ billingCycle: input.billingCycle,
258
+ features: version.features ?? [],
259
+ quotas: version.quotas ?? {}
260
+ }),
261
+ promotion: promotionFor(promotions, version.bundleKey, "BUNDLE", input, priceNet, asOf)
262
+ };
263
+ }
264
+ line(fields) {
265
+ return {
266
+ kind: fields.kind,
267
+ sourceKey: fields.sourceKey,
268
+ sourceVersionId: fields.sourceVersionId,
269
+ titleSnapshot: fields.title,
270
+ descriptionSnapshot: null,
271
+ quantity: 1,
272
+ unit: null,
273
+ priceNet: fields.priceNet,
274
+ priceGross: grossFromNet(fields.priceNet, this.settings.vatRate),
275
+ billingCycle: fields.billingCycle,
276
+ featuresSnapshot: [
277
+ ...fields.features
278
+ ],
279
+ quotaEffectsSnapshot: {
280
+ ...fields.quotas
281
+ },
282
+ metadata: null
283
+ };
284
+ }
285
+ /**
286
+ * The promo code's discount on the plan, after its promotion.
287
+ *
288
+ * Eligibility is the promo module's to decide, and it decides on its own
289
+ * terms: status, validity, the plan and rhythm the code is limited to. Only
290
+ * the amount is computed here, on the price this offer actually carries.
291
+ */
292
+ async pricePromoCode(input, planNet, vatRate) {
293
+ if (!input.promoCode) return null;
294
+ if (!this.promoCodes) throw promoCodeNotAccepted("PROMO_CODES_NOT_AVAILABLE");
295
+ const preview = await this.promoCodes.preview({
296
+ code: input.promoCode,
297
+ planId: input.planKey,
298
+ billingCycle: wireCycle(input.billingCycle)
299
+ }, {
300
+ checkoutOfferId: input.checkoutOfferId,
301
+ concluding: input.concluding
302
+ });
303
+ if (!preview.valid) throw promoCodeNotAccepted(preview.reason);
304
+ const planGross = grossFromNet(planNet, vatRate);
305
+ const discountGross = Math.min(computeDiscountGross({
306
+ gross: planGross
307
+ }, preview.discount), planGross);
308
+ const resolvedAmountNet = Math.min(planNet, netFromGross(discountGross, vatRate));
309
+ return {
310
+ code: preview.code,
311
+ label: preview.label,
312
+ valueType: preview.discount.valueType,
313
+ value: Number(preview.discount.value),
314
+ resolvedAmountNet,
315
+ durationType: preview.discount.durationType,
316
+ durationValue: preview.discount.durationValue
317
+ };
318
+ }
319
+ };
320
+ CheckoutOfferPricing = _ts_decorate([
321
+ Injectable(),
322
+ _ts_param(0, Inject(PLAN_CATALOG_SETTINGS_TOKEN)),
323
+ _ts_param(1, Inject(PLAN_REPOSITORY_TOKEN)),
324
+ _ts_param(2, Optional()),
325
+ _ts_param(2, Inject(BUNDLE_REPOSITORY_TOKEN)),
326
+ _ts_param(3, Optional()),
327
+ _ts_param(3, Inject(PROMOTION_REPOSITORY_TOKEN)),
328
+ _ts_param(4, Optional()),
329
+ _ts_param(4, Inject(PromoCodesService)),
330
+ _ts_metadata("design:type", Function),
331
+ _ts_metadata("design:paramtypes", [
332
+ typeof PlanCatalogSettings === "undefined" ? Object : PlanCatalogSettings,
333
+ typeof PlanRepository === "undefined" ? Object : PlanRepository,
334
+ Object,
335
+ Object,
336
+ Object
337
+ ])
338
+ ], CheckoutOfferPricing);
339
+ function promotionFor(promotions, targetKey, targetType, input, priceNet, asOf) {
340
+ const shown = promotionOnPrice(promotions, targetKey, input.locale, input.billingCycle, priceNet, asOf, targetType);
341
+ if (!shown) return null;
342
+ const { promotion } = shown;
343
+ const resolvedAmountNet = round2(Math.max(0, priceNet - shown.result.discounted));
344
+ if (resolvedAmountNet <= 0) return null;
345
+ const texts = promotion.i18n?.[input.locale] ?? promotion.i18n?.[DEFAULT_LOCALE] ?? {};
346
+ return {
347
+ id: promotion.id,
348
+ type: promotion.type,
349
+ value: promotion.value,
350
+ label: texts.badge || promotion.internalLabel,
351
+ resolvedAmountNet,
352
+ appliesTo: [
353
+ targetKey
354
+ ],
355
+ billingCycle: promotion.billingCycle
356
+ };
357
+ }
358
+ __name(promotionFor, "promotionFor");
359
+ function priceOf(version, billingCycle) {
360
+ const raw = billingCycle === "yearly" ? version.yearlyNet : version.monthlyNet;
361
+ if (raw === null || raw === void 0) return null;
362
+ const parsed = Number.parseFloat(raw);
363
+ return Number.isFinite(parsed) ? parsed : null;
364
+ }
365
+ __name(priceOf, "priceOf");
366
+ function wireCycle(billingCycle) {
367
+ return billingCycle === "yearly" ? "YEARLY" : "MONTHLY";
368
+ }
369
+ __name(wireCycle, "wireCycle");
370
+ function moneyOf(offer) {
371
+ const b = offer.priceBreakdown;
372
+ return JSON.stringify({
373
+ planVersionId: offer.planVersionId,
374
+ bundleVersionIds: offer.bundleVersionIds ?? [],
375
+ promotionId: offer.promotionId,
376
+ promoCode: offer.promoCode,
377
+ breakdown: [
378
+ b.currency,
379
+ b.billingCycle,
380
+ b.planNet,
381
+ b.bundlesNet,
382
+ b.regularNet,
383
+ b.effectiveNet,
384
+ b.vatRate,
385
+ b.effectiveGross
386
+ ],
387
+ // A line's gross is not compared: it is its share of the tax on the
388
+ // total, derived from the nets and the breakdown compared here, and the
389
+ // contract derives it again when it is written. So an offer whose stored
390
+ // lines carry a gross rounded another way still concludes, rather than
391
+ // being refused over a cent that moved between two of its lines.
392
+ lines: (offer.lineItems ?? []).map((item) => [
393
+ item.kind,
394
+ item.sourceKey,
395
+ item.sourceVersionId ?? null,
396
+ item.quantity,
397
+ item.priceNet,
398
+ item.billingCycle
399
+ ]),
400
+ promotions: (offer.promotionSnapshots ?? []).map((p) => [
401
+ p.id,
402
+ p.resolvedAmountNet
403
+ ]),
404
+ promoCodeDiscount: offer.promoCodeSnapshot ? [
405
+ offer.promoCodeSnapshot.code,
406
+ offer.promoCodeSnapshot.resolvedAmountNet
407
+ ] : null
408
+ });
409
+ }
410
+ __name(moneyOf, "moneyOf");
411
+ function planNotOffered(planKey, billingCycle) {
412
+ return new UnprocessableEntityException({
413
+ code: CONTRACT_ERROR_CODES.CHECKOUT_OFFER_PLAN_NOT_OFFERED,
414
+ message: `Plan '${planKey}' is not offered with a ${billingCycle} price at the moment.`,
415
+ params: {
416
+ planKey,
417
+ billingCycle
418
+ }
419
+ });
420
+ }
421
+ __name(planNotOffered, "planNotOffered");
422
+ function bundleNotOffered(bundleVersionId, reason) {
423
+ return new UnprocessableEntityException({
424
+ code: CONTRACT_ERROR_CODES.CHECKOUT_OFFER_BUNDLE_NOT_OFFERED,
425
+ message: `Bundle version '${bundleVersionId}' cannot be added to this offer (${reason}).`,
426
+ params: {
427
+ bundleVersionId,
428
+ reason
429
+ }
430
+ });
431
+ }
432
+ __name(bundleNotOffered, "bundleNotOffered");
433
+ function refusesThePromoCode(error) {
434
+ if (!(error instanceof UnprocessableEntityException)) return false;
435
+ const body = error.getResponse();
436
+ return typeof body === "object" && body !== null && body.code === CONTRACT_ERROR_CODES.CHECKOUT_OFFER_PROMO_CODE_NOT_ACCEPTED;
437
+ }
438
+ __name(refusesThePromoCode, "refusesThePromoCode");
439
+ function promoCodeNotAccepted(reason) {
440
+ return new UnprocessableEntityException({
441
+ code: CONTRACT_ERROR_CODES.CHECKOUT_OFFER_PROMO_CODE_NOT_ACCEPTED,
442
+ message: `The promo code cannot be applied to this offer (${reason}).`,
443
+ params: {
444
+ reason
445
+ }
446
+ });
447
+ }
448
+ __name(promoCodeNotAccepted, "promoCodeNotAccepted");
449
+
450
+ // src/checkout-offer/checkout-offer.tokens.ts
451
+ var CHECKOUT_OFFER_REPOSITORY_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/nest/CheckoutOfferRepository");
452
+ var CHECKOUT_OFFER_TRANSACTION_RUNNER_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/nest/CheckoutOfferTransactionRunner");
453
+
454
+ // src/checkout-offer/checkout-offer.service.ts
455
+ import { isDeepStrictEqual } from "util";
456
+ import { ConflictException, Inject as Inject2, Injectable as Injectable2, NotFoundException, Optional as Optional2, UnprocessableEntityException as UnprocessableEntityException2 } from "@nestjs/common";
457
+ import { CONTRACT_ERROR_CODES as CONTRACT_ERROR_CODES2, SUBSCRIBER_ERROR_CODES, buildFeatureRequiresIndex, collectUnsatisfiedRequires } from "@saasicat/core";
458
+ function _ts_decorate2(decorators, target, key, desc) {
459
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
460
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
461
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
462
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
463
+ }
464
+ __name(_ts_decorate2, "_ts_decorate");
465
+ function _ts_metadata2(k, v) {
466
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
467
+ }
468
+ __name(_ts_metadata2, "_ts_metadata");
469
+ function _ts_param2(paramIndex, decorator) {
470
+ return function(target, key) {
471
+ decorator(target, key, paramIndex);
472
+ };
473
+ }
474
+ __name(_ts_param2, "_ts_param");
475
+ var DEFAULT_OFFER_LOCALE = "de";
476
+ var HOLD_ATTEMPTS = 3;
477
+ var CheckoutOfferService = class {
478
+ static {
479
+ __name(this, "CheckoutOfferService");
480
+ }
481
+ repo;
482
+ pricing;
483
+ bundles;
484
+ plans;
485
+ catalogEntries;
486
+ contracts;
487
+ transactions;
488
+ subscribers;
489
+ promoCodes;
490
+ constructor(repo, pricing, bundles = null, plans = null, catalogEntries = null, contracts = null, transactions = null, subscribers = null, promoCodes = null) {
491
+ this.repo = repo;
492
+ this.pricing = pricing;
493
+ this.bundles = bundles;
494
+ this.plans = plans;
495
+ this.catalogEntries = catalogEntries;
496
+ this.contracts = contracts;
497
+ this.transactions = transactions;
498
+ this.subscribers = subscribers;
499
+ this.promoCodes = promoCodes;
500
+ }
501
+ list(filter) {
502
+ return this.repo.list(filter);
503
+ }
504
+ async getById(id) {
505
+ const row = await this.repo.findById(id);
506
+ if (!row) {
507
+ throw new NotFoundException({
508
+ code: CONTRACT_ERROR_CODES2.CHECKOUT_OFFER_NOT_FOUND,
509
+ message: `CheckoutOffer '${id}' not found`,
510
+ params: {
511
+ offerId: id
512
+ }
513
+ });
514
+ }
515
+ return row;
516
+ }
517
+ /** Prices what the caller chose and stores it as an open offer. */
518
+ async create(selection) {
519
+ const locale = selection.locale ?? DEFAULT_OFFER_LOCALE;
520
+ const priced = await this.pricing.price({
521
+ planKey: selection.planKey,
522
+ billingCycle: selection.billingCycle,
523
+ bundleVersionIds: selection.bundleVersionIds ?? [],
524
+ promoCode: selection.promoCode ?? null,
525
+ locale
526
+ });
527
+ await this.assertFeatureRequiresSatisfied({
528
+ planKey: selection.planKey,
529
+ planVersionId: priced.planVersionId,
530
+ bundleVersionIds: priced.bundleVersionIds,
531
+ lineItems: priced.lineItems
532
+ });
533
+ return this.repo.create({
534
+ planKey: selection.planKey,
535
+ billingCycle: selection.billingCycle,
536
+ locale,
537
+ validUntil: selection.validUntil ?? null,
538
+ ...priced
539
+ });
540
+ }
541
+ /**
542
+ * Customization in onboarding — only while the offer is `open`. The plan
543
+ * stays; whatever else changes, the whole offer is priced again. A slot of
544
+ * its promo code held for its checkout stays with it while the code does,
545
+ * and is given back when the code is changed or removed.
546
+ */
547
+ async update(id, change) {
548
+ const existing = await this.getById(id);
549
+ this.assertOpen(existing, "changed");
550
+ const billingCycle = change.billingCycle ?? existing.billingCycle;
551
+ const locale = change.locale ?? existing.locale;
552
+ const priced = await this.pricing.price({
553
+ planKey: existing.planKey,
554
+ billingCycle,
555
+ bundleVersionIds: change.bundleVersionIds ?? existing.bundleVersionIds ?? [],
556
+ promoCode: change.promoCode !== void 0 ? change.promoCode : existing.promoCode,
557
+ locale,
558
+ checkoutOfferId: id
559
+ });
560
+ await this.assertFeatureRequiresSatisfied({
561
+ planKey: existing.planKey,
562
+ planVersionId: priced.planVersionId,
563
+ bundleVersionIds: priced.bundleVersionIds,
564
+ lineItems: priced.lineItems
565
+ });
566
+ const codeChanges = priced.promoCode !== existing.promoCode;
567
+ const updated = await this.repo.update(id, {
568
+ billingCycle,
569
+ locale,
570
+ ...change.validUntil !== void 0 ? {
571
+ validUntil: change.validUntil
572
+ } : {},
573
+ ...priced
574
+ });
575
+ if (codeChanges) await this.promoCodes?.releaseCheckoutHold(id);
576
+ return updated;
577
+ }
578
+ /**
579
+ * Holds a slot of the offer's promo code for its checkout until `until` —
580
+ * what a sign-up does as its checkout starts, until a confirmation of its
581
+ * payment form can no longer arrive — so that the code cannot run out
582
+ * before the payment is confirmed and the offer concluded. A live slot the
583
+ * offer holds of that code already moves to `until`, never earlier. The
584
+ * conclusion turns the slot into the redemption; the slot goes back when
585
+ * the offer's code changes or `until` passes.
586
+ *
587
+ * Refused, and nothing held, when the offer is no longer open or its code
588
+ * cannot be held: `PROMO_CODE_NOT_REDEEMABLE` with the reason a preview
589
+ * gives, `EXHAUSTED` when every slot is redeemed or held. An offer without
590
+ * a code holds nothing.
591
+ */
592
+ async holdPromoCode(id, hold) {
593
+ for (let attempt = 0; attempt < HOLD_ATTEMPTS; attempt += 1) {
594
+ const offer = await this.getById(id);
595
+ this.assertOpen(offer, "held");
596
+ const code = offer.promoCode;
597
+ if (!code) {
598
+ await this.promoCodes?.releaseCheckoutHold(id);
599
+ return;
600
+ }
601
+ if (!this.promoCodes) {
602
+ throw new Error(`Checkout offer '${id}' carries a promo code and no promo module is registered to hold it: enable \`promoCodes\` in SaaSiCatModule.forRoot, or register PromoCodesModule globally beside CheckoutOfferModule.`);
603
+ }
604
+ await this.promoCodes.holdForCheckout({
605
+ checkoutOfferId: id,
606
+ code,
607
+ planId: offer.planKey,
608
+ billingCycle: offer.billingCycle === "yearly" ? "YEARLY" : "MONTHLY",
609
+ email: hold.email,
610
+ until: hold.until
611
+ });
612
+ if ((await this.getById(id)).promoCode === code) return;
613
+ }
614
+ await this.promoCodes?.releaseCheckoutHold(id);
615
+ throw offerChanged(id, "its promo code was being held");
616
+ }
617
+ /**
618
+ * Gives back the slot held for the offer's checkout while it is still held
619
+ * until `heldUntil`, as the caller wrote it. A slot another start of the
620
+ * checkout moved later stays with the form that start opened.
621
+ */
622
+ async releasePromoCodeHold(id, heldUntil) {
623
+ await this.promoCodes?.releaseCheckoutHoldIfUnmoved(id, heldUntil);
624
+ }
625
+ /**
626
+ * Freezes the offer (`status = 'consumed'`). Returns the final
627
+ * snapshot — the caller (registration/billing) writes it
628
+ * as `Subscription.packageSnapshot`.
629
+ *
630
+ * Refused unless every add-on is still bookable and the stored amounts are
631
+ * what the catalogue makes of the offer's own selection, so an offer whose
632
+ * amounts were written by anything but the pricing never becomes a contract.
633
+ *
634
+ * The plan, the add-ons and the promotions are priced as they stood when the
635
+ * offer was priced; a promo code is checked with the promo module as it
636
+ * stands now. A code is redeemed when the contract is concluded, so one that
637
+ * has expired or run out of redemptions since refuses the offer with
638
+ * `CHECKOUT_OFFER_PROMO_CODE_NOT_ACCEPTED`; `CHECKOUT_OFFER_PRICE_NOT_CURRENT`
639
+ * is kept for amounts that no longer match.
640
+ */
641
+ async consume(id) {
642
+ await this.assertConsumable(id);
643
+ return this.repo.consume(id);
644
+ }
645
+ /**
646
+ * Consumes an offer and creates its contract in one transaction, together
647
+ * with the application's own writes in `within`, so an offer is never
648
+ * consumed without its contract, and no contract is written for an offer
649
+ * that stays open.
650
+ *
651
+ * Everything that can refuse runs before the transaction: what `consume`
652
+ * checks, and the checks the contract the offer becomes is held to. Inside
653
+ * it, the offer is consumed on the condition that it is still open, the
654
+ * contract is written, and `within` runs; an error in any of them undoes
655
+ * the others, and the offer can be concluded again.
656
+ *
657
+ * An offer that is already concluded for the same tenant answers with its
658
+ * contract rather than a refusal, so a caller retrying after a lost answer,
659
+ * or losing a race to another call for that tenant, gets the conclusion that
660
+ * stands; `within` does not run again, its writes committed with it. A
661
+ * conclusion for another tenant is refused with
662
+ * `CHECKOUT_OFFER_ALREADY_CONSUMED`, and an offer changed between the checks
663
+ * and the transaction with `CHECKOUT_OFFER_CHANGED`, nothing written.
664
+ *
665
+ * The contract is between the tenant's subscriber and the configured
666
+ * issuer. A sign-up creates the tenant in `within` and has no subscriber
667
+ * yet, so it passes `subscriber`, which is created on the same transaction
668
+ * before the contract; whether one may be created, or whether the tenant
669
+ * has one, is asked before the transaction with the other checks.
670
+ *
671
+ * A promo code on the offer is redeemed in `within`, with
672
+ * `PromoCodesService.redeemInTransaction` on the same transaction. Before
673
+ * the transaction the code is only checked the way pricing checks it; the
674
+ * redemption checks the rest, the customer and the remaining redemptions,
675
+ * where a refusal still undoes everything. Nothing checks the code again
676
+ * after `within`, so a redemption that takes its last slot does not refuse
677
+ * the offer it was redeemed for.
678
+ *
679
+ * A slot held for the offer's checkout (`holdPromoCode`) is the offer's own
680
+ * throughout: the checks before the transaction do not count it against
681
+ * the code and accept the code as it stood when the slot was held, and the
682
+ * redemption in `within` takes that slot rather than a free one. A slot
683
+ * `within` did not redeem is given back before the conclusion commits.
684
+ */
685
+ async conclude(id, options, within) {
686
+ const { contracts, transactions, subscribers } = this.conclusionPorts();
687
+ const { subscriber, tx: callersTransaction, ...contractOptions } = options;
688
+ const { tenantId } = contractOptions;
689
+ const standing = await this.standingConclusion(await this.getById(id), contracts, tenantId);
690
+ if (standing) return standing;
691
+ const existing = await this.assertConsumable(id);
692
+ const checked = contracts.prepareFromOffer(existing, contractOptions);
693
+ await this.assertParty(subscribers, contracts, tenantId, subscriber);
694
+ let consumeFailed = false;
695
+ const concludeOn = /* @__PURE__ */ __name(async (tx) => {
696
+ consumeFailed = false;
697
+ let offer;
698
+ try {
699
+ offer = await this.repo.consume(id, tx);
700
+ } catch (error) {
701
+ consumeFailed = true;
702
+ throw error;
703
+ }
704
+ const data = contracts.createDataFromOffer(offer, contractOptions);
705
+ if (!isDeepStrictEqual(data, checked)) throw offerChanged(id);
706
+ if (subscriber) await subscribers.createForTenant(tenantId, subscriber, tx);
707
+ const contract = await contracts.create(data, tx);
708
+ const concluded = {
709
+ offer,
710
+ contract
711
+ };
712
+ const handedOver = await this.promoCodes?.handOverCheckoutHold(id, tx) ?? false;
713
+ if (within) await within(tx, concluded);
714
+ if (handedOver) await this.promoCodes?.releaseCheckoutHold(id, tx);
715
+ return concluded;
716
+ }, "concludeOn");
717
+ try {
718
+ return await (callersTransaction === void 0 ? transactions.run(concludeOn) : concludeOn(callersTransaction));
719
+ } catch (error) {
720
+ if (!consumeFailed) throw error;
721
+ const offer = await this.repo.findById(id).catch(() => null);
722
+ const concludedMeanwhile = await this.standingConclusion(offer, contracts, tenantId).catch(() => null);
723
+ if (concludedMeanwhile) return concludedMeanwhile;
724
+ throw error;
725
+ }
726
+ }
727
+ async assertConsumable(id) {
728
+ const existing = await this.getById(id);
729
+ this.assertOpen(existing, "consumed");
730
+ await this.assertBundleVersionsStillBookable(existing);
731
+ await this.pricing.assertPricedByCatalogue(existing);
732
+ return existing;
733
+ }
734
+ /**
735
+ * The conclusion that stands for this tenant, if the offer has one. An offer
736
+ * carries no tenant and its id travels in a link, so a conclusion for
737
+ * another tenant is not an answer: that caller is refused as before.
738
+ */
739
+ async standingConclusion(offer, contracts, tenantId) {
740
+ if (!offer || offer.status !== "consumed") return null;
741
+ const contract = await contracts.findByOriginalOfferId(offer.id);
742
+ return contract && contract.tenantId === tenantId ? {
743
+ offer,
744
+ contract
745
+ } : null;
746
+ }
747
+ conclusionPorts() {
748
+ if (!this.contracts || !this.transactions || !this.subscribers) {
749
+ throw new Error("CheckoutOfferService.conclude needs the subscription contract repository, the subscriber repository and a transaction runner: pass `conclusion` to CheckoutOfferModule.forRoot, or give SaaSiCatModule.forRoot a persistence bundle that has all three.");
750
+ }
751
+ return {
752
+ contracts: this.contracts,
753
+ transactions: this.transactions,
754
+ subscribers: this.subscribers
755
+ };
756
+ }
757
+ /**
758
+ * Before anything is written: the subscriber to create is one that may be
759
+ * created, or the tenant already has the party the contract is with.
760
+ */
761
+ async assertParty(subscribers, contracts, tenantId, subscriber) {
762
+ if (!subscriber) {
763
+ await contracts.assertPartyFor(tenantId);
764
+ return;
765
+ }
766
+ settleNewSubscriberDetails(subscriber);
767
+ if (await subscribers.findByTenantId(tenantId)) {
768
+ throw new ConflictException(codedError(SUBSCRIBER_ERROR_CODES.SUBSCRIBER_ALREADY_EXISTS, {
769
+ tenantId
770
+ }));
771
+ }
772
+ }
773
+ /**
774
+ * #35 P6 — server-side requires validation: the dependencies of all
775
+ * features (plan ∪ selected bundles) must be covered within the
776
+ * selection, otherwise an offer would be created whose features cannot
777
+ * work at the tenant (an app-specific validateModuleDependencies check is
778
+ * thereby replaced on the platform side). The requires source are the
779
+ * curated FeatureCatalogEntries; without CatalogEntryRepository it is
780
+ * skipped (graceful — no requires data available).
781
+ */
782
+ async assertFeatureRequiresSatisfied(input) {
783
+ if (!this.catalogEntries) return;
784
+ const entries = await this.catalogEntries.listFeatures({});
785
+ const requiresIndex = buildFeatureRequiresIndex(entries);
786
+ if (requiresIndex.size === 0) return;
787
+ const selected = /* @__PURE__ */ new Set([
788
+ ...await this.resolvePlanFeatures(input),
789
+ ...await this.resolveBundleFeatures(input)
790
+ ]);
791
+ const missingRequires = collectUnsatisfiedRequires([
792
+ ...selected
793
+ ], requiresIndex);
794
+ if (missingRequires.length > 0) {
795
+ throw new UnprocessableEntityException2({
796
+ code: CONTRACT_ERROR_CODES2.CHECKOUT_OFFER_FEATURE_DEPENDENCY_UNSATISFIED,
797
+ message: `The selected plan does not cover all feature dependencies: [${missingRequires.join(", ")}] are missing from the plan + selected bundles.`,
798
+ missingRequires
799
+ });
800
+ }
801
+ }
802
+ /**
803
+ * Plan features preferably from the server SSOT (PlanRepository); fallback
804
+ * is the frozen featuresSnapshot of the plan LineItem (adapter without
805
+ * PlanRepository wiring).
806
+ */
807
+ async resolvePlanFeatures(input) {
808
+ if (this.plans) {
809
+ const version = input.planVersionId ? await this.plans.findVersionById?.(input.planVersionId) : null;
810
+ const resolved = version ?? await this.plans.findActivePlanVersion?.(input.planKey) ?? await this.plans.findLatestLivePlanVersion?.(input.planKey);
811
+ if (resolved) return resolved.features ?? [];
812
+ }
813
+ return input.lineItems.filter((item) => item.kind === "plan").flatMap((item) => item.featuresSnapshot ?? []);
814
+ }
815
+ /** Bundle features from the BundleVersions; fallback featuresSnapshot. */
816
+ async resolveBundleFeatures(input) {
817
+ if (this.bundles) {
818
+ const features = [];
819
+ let allResolved = true;
820
+ for (const bundleVersionId of input.bundleVersionIds) {
821
+ const version = await this.bundles.findVersionById(bundleVersionId);
822
+ if (!version) {
823
+ allResolved = false;
824
+ break;
825
+ }
826
+ features.push(...version.features ?? []);
827
+ }
828
+ if (allResolved) return features;
829
+ }
830
+ return input.lineItems.filter((item) => item.kind === "bundle").flatMap((item) => item.featuresSnapshot ?? []);
831
+ }
832
+ async assertBundleVersionsStillBookable(offer) {
833
+ if (!this.bundles || !offer.bundleVersionIds || offer.bundleVersionIds.length === 0) {
834
+ return;
835
+ }
836
+ const now = Date.now();
837
+ const violations = [];
838
+ for (const bundleVersionId of offer.bundleVersionIds) {
839
+ const version = await this.bundles.findVersionById(bundleVersionId);
840
+ if (!version) {
841
+ violations.push({
842
+ bundleVersionId,
843
+ reason: "missing"
844
+ });
845
+ continue;
846
+ }
847
+ const reason = bundleVersionNotBookableReason(version, now);
848
+ if (reason) violations.push({
849
+ bundleVersionId,
850
+ reason
851
+ });
852
+ }
853
+ if (violations.length > 0) {
854
+ throw new UnprocessableEntityException2({
855
+ code: CONTRACT_ERROR_CODES2.CHECKOUT_OFFER_BUNDLE_VERSION_NOT_BOOKABLE,
856
+ message: "At least one bundle version from the checkout offer is no longer bookable.",
857
+ violations
858
+ });
859
+ }
860
+ }
861
+ assertOpen(existing, action) {
862
+ if (existing.status === "consumed") {
863
+ throw new ConflictException({
864
+ code: CONTRACT_ERROR_CODES2.CHECKOUT_OFFER_ALREADY_CONSUMED,
865
+ message: `Checkout offer '${existing.id}' has already been consumed and cannot be ${action}`,
866
+ params: {
867
+ offerId: existing.id,
868
+ action
869
+ }
870
+ });
871
+ }
872
+ if (existing.status === "expired") {
873
+ throw new ConflictException({
874
+ code: CONTRACT_ERROR_CODES2.CHECKOUT_OFFER_EXPIRED,
875
+ message: `Checkout offer '${existing.id}' has expired and cannot be ${action}`,
876
+ params: {
877
+ offerId: existing.id,
878
+ action,
879
+ validUntil: existing.validUntil ?? null
880
+ }
881
+ });
882
+ }
883
+ if (this.isExpired(existing)) {
884
+ throw new ConflictException({
885
+ code: CONTRACT_ERROR_CODES2.CHECKOUT_OFFER_EXPIRED,
886
+ message: `CheckoutOffer '${existing.id}' has expired and cannot be ${action}`,
887
+ params: {
888
+ offerId: existing.id,
889
+ action,
890
+ validUntil: existing.validUntil ?? null
891
+ }
892
+ });
893
+ }
894
+ }
895
+ isExpired(row) {
896
+ return isValidUntilExpired(row.validUntil, Date.now());
897
+ }
898
+ };
899
+ CheckoutOfferService = _ts_decorate2([
900
+ Injectable2(),
901
+ _ts_param2(0, Inject2(CHECKOUT_OFFER_REPOSITORY_TOKEN)),
902
+ _ts_param2(1, Inject2(CheckoutOfferPricing)),
903
+ _ts_param2(2, Optional2()),
904
+ _ts_param2(2, Inject2(BUNDLE_REPOSITORY_TOKEN)),
905
+ _ts_param2(3, Optional2()),
906
+ _ts_param2(3, Inject2(PLAN_REPOSITORY_TOKEN)),
907
+ _ts_param2(4, Optional2()),
908
+ _ts_param2(4, Inject2(CATALOG_ENTRY_REPOSITORY_TOKEN)),
909
+ _ts_param2(5, Optional2()),
910
+ _ts_param2(5, Inject2(SubscriptionContractService)),
911
+ _ts_param2(6, Optional2()),
912
+ _ts_param2(6, Inject2(CHECKOUT_OFFER_TRANSACTION_RUNNER_TOKEN)),
913
+ _ts_param2(7, Optional2()),
914
+ _ts_param2(7, Inject2(SubscriberService)),
915
+ _ts_param2(8, Optional2()),
916
+ _ts_param2(8, Inject2(PromoCodesService)),
917
+ _ts_metadata2("design:type", Function),
918
+ _ts_metadata2("design:paramtypes", [
919
+ typeof CheckoutOfferRepository === "undefined" ? Object : CheckoutOfferRepository,
920
+ typeof CheckoutOfferPricing === "undefined" ? Object : CheckoutOfferPricing,
921
+ Object,
922
+ Object,
923
+ Object,
924
+ Object,
925
+ Object,
926
+ Object,
927
+ Object
928
+ ])
929
+ ], CheckoutOfferService);
930
+ function offerChanged(offerId, during = "it was being concluded") {
931
+ return new ConflictException({
932
+ code: CONTRACT_ERROR_CODES2.CHECKOUT_OFFER_CHANGED,
933
+ message: `Checkout offer '${offerId}' changed while ${during}. Load it again.`,
934
+ params: {
935
+ offerId
936
+ }
937
+ });
938
+ }
939
+ __name(offerChanged, "offerChanged");
940
+
941
+ export {
942
+ CheckoutOfferPricing,
943
+ CHECKOUT_OFFER_REPOSITORY_TOKEN,
944
+ CHECKOUT_OFFER_TRANSACTION_RUNNER_TOKEN,
945
+ CheckoutOfferService
946
+ };