@saasicat/nest 1.0.0-rc.20 → 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 (112) hide show
  1. package/dist/.build-stamp +1 -1
  2. package/dist/_entries.cjs +3570 -2574
  3. package/dist/admin/index.d.cts +5 -4
  4. package/dist/admin/index.d.ts +5 -4
  5. package/dist/admin/index.js +2 -1
  6. package/dist/{admin-resources.module-aOJ_0rur.d.cts → admin-resources.module-B4XNvdH-.d.cts} +0 -1
  7. package/dist/{admin-resources.module-BENMc0Wh.d.ts → admin-resources.module-CWqdI9BV.d.ts} +0 -1
  8. package/dist/{admin-stats.service-cwLMrkGl.d.ts → admin-stats.service-CsLNt19Z.d.ts} +10 -6
  9. package/dist/{admin-stats.service-BJdzHCtJ.d.cts → admin-stats.service-DrZrU8IJ.d.cts} +10 -6
  10. package/dist/{aggregation-CtIjlWHT.d.ts → aggregation-ChpZDh0g.d.ts} +20 -4
  11. package/dist/{aggregation-BHo7a_In.d.cts → aggregation-OpnhwzF5.d.cts} +20 -4
  12. package/dist/billing/index.d.cts +23 -16
  13. package/dist/billing/index.d.ts +23 -16
  14. package/dist/billing/index.js +20 -15
  15. package/dist/catalog/index.d.cts +17 -8
  16. package/dist/catalog/index.d.ts +17 -8
  17. package/dist/catalog/index.js +5 -3
  18. package/dist/{catalog.module-B5E8tsl7.d.ts → catalog.module-C9O7BxRq.d.ts} +80 -7
  19. package/dist/{catalog.module-DKVvsPsl.d.cts → catalog.module-T-I5Ricz.d.cts} +80 -7
  20. package/dist/checkout-offer/index.d.cts +5 -4
  21. package/dist/checkout-offer/index.d.ts +5 -4
  22. package/dist/checkout-offer/index.js +15 -14
  23. package/dist/{checkout-offer.service-BP4CoZle.d.ts → checkout-offer.service-BeHgYikI.d.ts} +51 -8
  24. package/dist/{checkout-offer.service-Bk5mHEoR.d.cts → checkout-offer.service-CCVxG9b2.d.cts} +51 -8
  25. package/dist/{chunk-AOQJEYOL.js → chunk-3QVGA6JX.js} +3 -1
  26. package/dist/chunk-3RLWML2S.js +896 -0
  27. package/dist/{chunk-DTIYUJH5.js → chunk-4SDSYV4A.js} +16 -6
  28. package/dist/{chunk-X4PDUYBX.js → chunk-55QEDAMX.js} +106 -302
  29. package/dist/chunk-6PJE7EWO.js +185 -0
  30. package/dist/{chunk-CRCNY3U6.js → chunk-7NJKVC5X.js} +54 -14
  31. package/dist/{chunk-RSAPMUGB.js → chunk-AOS2JPDD.js} +67 -64
  32. package/dist/{chunk-NCXGXNQK.js → chunk-CQ2ZZMTD.js} +174 -72
  33. package/dist/{chunk-NEICZJE4.js → chunk-F5X66HF5.js} +1 -1
  34. package/dist/{chunk-323FB6TD.js → chunk-FEQZCWUL.js} +67 -19
  35. package/dist/{chunk-FQS5FPH2.js → chunk-G6EZWECL.js} +42 -21
  36. package/dist/{chunk-XDRJ4FRG.js → chunk-JBCC6C3M.js} +84 -20
  37. package/dist/chunk-LF6J4YYN.js +301 -0
  38. package/dist/{chunk-OAUN2FPJ.js → chunk-LLYVYRGJ.js} +50 -21
  39. package/dist/{chunk-6ISJMNW7.js → chunk-M47BNEY2.js} +125 -108
  40. package/dist/{chunk-U465NU23.js → chunk-NDLC5GYK.js} +40 -33
  41. package/dist/{chunk-YFCTBLM5.js → chunk-QPVDCKYS.js} +237 -60
  42. package/dist/{chunk-IORLRWJZ.js → chunk-R5YCJBHY.js} +2 -2
  43. package/dist/{chunk-QBF7KLSA.js → chunk-TBBZWZQT.js} +8 -6
  44. package/dist/chunk-TOEFN7DN.js +295 -0
  45. package/dist/{chunk-ZUOCCEWB.js → chunk-WHJSYKNC.js} +2 -2
  46. package/dist/chunk-WOEJ6K7M.js +55 -0
  47. package/dist/{chunk-WYXLTSUZ.js → chunk-XSSWYPP5.js} +6 -4
  48. package/dist/contract-line-item-money-B0Z3Ogur.d.cts +43 -0
  49. package/dist/contract-line-item-money-B0Z3Ogur.d.ts +43 -0
  50. package/dist/{enforce-quota.interceptor-CLyCNVIm.d.cts → enforce-quota.interceptor-BFyVdWe8.d.cts} +6 -6
  51. package/dist/{enforce-quota.interceptor-DQ_BN_jV.d.ts → enforce-quota.interceptor-GaBXboGx.d.ts} +6 -6
  52. package/dist/entitlement/index.d.cts +2 -1
  53. package/dist/entitlement/index.d.ts +2 -1
  54. package/dist/entitlement/index.js +6 -7
  55. package/dist/index.d.cts +27 -26
  56. package/dist/index.d.ts +27 -26
  57. package/dist/index.js +47 -33
  58. package/dist/{issuer-identity.check-B8Wzugbf.d.cts → issuer-identity.check-CBO0vcsq.d.cts} +5 -5
  59. package/dist/{issuer-identity.check-q-Uha5XY.d.ts → issuer-identity.check-Cz9CEMkZ.d.ts} +5 -5
  60. package/dist/{module-options-C5EBOxzm.d.ts → module-options-DmQ3G4sZ.d.ts} +17 -7
  61. package/dist/{module-options-Cr5FkELF.d.cts → module-options-qAOwinHR.d.cts} +17 -7
  62. package/dist/{payment-callback.service-DPUfjp-F.d.cts → payment-callback.service-CR84Xw1Z.d.cts} +31 -3
  63. package/dist/{payment-callback.service-DPUfjp-F.d.ts → payment-callback.service-CR84Xw1Z.d.ts} +31 -3
  64. package/dist/payments/index.d.cts +59 -5
  65. package/dist/payments/index.d.ts +59 -5
  66. package/dist/payments/index.js +11 -5
  67. package/dist/{payments.module-Da26YttL.d.cts → payments.module-BdUq9ot_.d.cts} +5 -2
  68. package/dist/{payments.module-C6r6dJdn.d.ts → payments.module-CQGv_VG7.d.ts} +5 -2
  69. package/dist/plan-catalog-source-DWe-BGY1.d.cts +21 -0
  70. package/dist/plan-catalog-source-DWe-BGY1.d.ts +21 -0
  71. package/dist/{plan-versions.service-BTlw-aEX.d.ts → plan-versions.service-C0N-Em2W.d.ts} +2 -48
  72. package/dist/{plan-versions.service-BM7lKLr1.d.cts → plan-versions.service-DARLh-qI.d.cts} +2 -48
  73. package/dist/platform/index.d.cts +20 -19
  74. package/dist/platform/index.d.ts +20 -19
  75. package/dist/platform/index.js +22 -21
  76. package/dist/promo/index.d.cts +9 -6
  77. package/dist/promo/index.d.ts +9 -6
  78. package/dist/promo/index.js +7 -6
  79. package/dist/{promo.module-YfF6ADDf.d.cts → promo.module-CKkECmU8.d.cts} +8 -2
  80. package/dist/{promo.module-KmIpp1Gb.d.ts → promo.module-DuIJdK3U.d.ts} +8 -2
  81. package/dist/promo.service-BYBu6dy3.d.cts +215 -0
  82. package/dist/promo.service-CFjfF0Xm.d.ts +215 -0
  83. package/dist/registration/index.d.cts +29 -4
  84. package/dist/registration/index.d.ts +29 -4
  85. package/dist/registration/index.js +12 -5
  86. package/dist/subscriber/index.d.cts +2 -2
  87. package/dist/subscriber/index.d.ts +2 -2
  88. package/dist/subscriber/index.js +5 -4
  89. package/dist/{subscriber.service-BZsvHw1V.d.cts → subscriber.service-5W6Gi-1B.d.cts} +14 -3
  90. package/dist/{subscriber.service-BZsvHw1V.d.ts → subscriber.service-5W6Gi-1B.d.ts} +14 -3
  91. package/dist/subscription-contract/index.d.cts +3 -3
  92. package/dist/subscription-contract/index.d.ts +3 -3
  93. package/dist/subscription-contract/index.js +11 -9
  94. package/dist/{subscription-contract.service-Duv_Ns1W.d.ts → subscription-contract.service-CPZrTM9C.d.ts} +1 -1
  95. package/dist/{subscription-contract.service-D7Qb_Ns-.d.cts → subscription-contract.service-CfNApLuP.d.cts} +1 -1
  96. package/dist/{tenant-billing.controller-0HZ_2DQH.d.ts → tenant-billing.controller-BLqbsONe.d.ts} +7 -6
  97. package/dist/{tenant-billing.controller-CN9DgasV.d.cts → tenant-billing.controller-DKbAqTbM.d.cts} +7 -6
  98. package/dist/{tenant-billing.module-CPybDf7c.d.cts → tenant-billing.module-ClbjzocC.d.cts} +24 -16
  99. package/dist/{tenant-billing.module-C1sLBCW-.d.ts → tenant-billing.module-DohgJJ2m.d.ts} +24 -16
  100. package/dist/testing/index.d.cts +11 -10
  101. package/dist/testing/index.d.ts +11 -10
  102. package/dist/testing/index.js +20 -20
  103. package/package.json +3 -3
  104. package/dist/chunk-3HT3REXB.js +0 -110
  105. package/dist/chunk-5BCYJ3QY.js +0 -44
  106. package/dist/chunk-QSBMFY4I.js +0 -33
  107. package/dist/chunk-U4X2YRRO.js +0 -214
  108. package/dist/chunk-XWAZNKAB.js +0 -638
  109. package/dist/contract-line-item-money-BWWQhRul.d.cts +0 -11
  110. package/dist/contract-line-item-money-BWWQhRul.d.ts +0 -11
  111. package/dist/promo.service-CBMm9RU7.d.cts +0 -113
  112. package/dist/promo.service-CBMm9RU7.d.ts +0 -113
@@ -2,6 +2,7 @@ import {
2
2
  AdminAuditService
3
3
  } from "./chunk-OOTEXP47.js";
4
4
  import {
5
+ PROMO_CODE_HOLD_REPOSITORY_TOKEN,
5
6
  PROMO_CODE_REDEMPTION_REPOSITORY_TOKEN,
6
7
  PROMO_CODE_REPOSITORY_TOKEN,
7
8
  PROMO_CODE_VALIDATION_LOG_REPOSITORY_TOKEN,
@@ -11,7 +12,7 @@ import {
11
12
  PROMO_SUBSCRIPTION_LOOKUP_TOKEN,
12
13
  PROMO_TRANSACTION_RUNNER_TOKEN,
13
14
  PromoCodesService
14
- } from "./chunk-XWAZNKAB.js";
15
+ } from "./chunk-3RLWML2S.js";
15
16
  import {
16
17
  SaaSiCatPublicRoute
17
18
  } from "./chunk-YSC46BGM.js";
@@ -24,7 +25,7 @@ import {
24
25
  } from "./chunk-SHUYVCID.js";
25
26
 
26
27
  // src/promo/promo-code-expirer.service.ts
27
- import { Inject, Injectable, Logger } from "@nestjs/common";
28
+ import { Inject, Injectable, Logger, Optional } from "@nestjs/common";
28
29
  import { Cron, CronExpression } from "@nestjs/schedule";
29
30
  function _ts_decorate(decorators, target, key, desc) {
30
31
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -49,17 +50,20 @@ var PromoCodeExpirer = class _PromoCodeExpirer {
49
50
  }
50
51
  promoRepo;
51
52
  redemptionRepo;
53
+ holds;
52
54
  logger = new Logger(_PromoCodeExpirer.name);
53
- constructor(promoRepo, redemptionRepo) {
55
+ constructor(promoRepo, redemptionRepo, holds = null) {
54
56
  this.promoRepo = promoRepo;
55
57
  this.redemptionRepo = redemptionRepo;
58
+ this.holds = holds;
56
59
  }
57
60
  async expirePromoCodes() {
58
61
  const now = /* @__PURE__ */ new Date();
59
62
  const codes = await this.promoRepo.expireDueCodes(now);
60
63
  const redemptions = await this.redemptionRepo.expireDueRedemptions(now);
61
- if (codes > 0 || redemptions > 0) {
62
- this.logger.log(`PromoCodeExpirer: ${codes} Codes, ${redemptions} redemptions expired.`);
64
+ const holds = await this.holds?.expireDue(now) ?? 0;
65
+ if (codes > 0 || redemptions > 0 || holds > 0) {
66
+ this.logger.log(`PromoCodeExpirer: ${codes} Codes, ${redemptions} redemptions, ${holds} holds expired.`);
63
67
  }
64
68
  }
65
69
  };
@@ -76,10 +80,13 @@ PromoCodeExpirer = _ts_decorate([
76
80
  Injectable(),
77
81
  _ts_param(0, Inject(PROMO_CODE_REPOSITORY_TOKEN)),
78
82
  _ts_param(1, Inject(PROMO_CODE_REDEMPTION_REPOSITORY_TOKEN)),
83
+ _ts_param(2, Optional()),
84
+ _ts_param(2, Inject(PROMO_CODE_HOLD_REPOSITORY_TOKEN)),
79
85
  _ts_metadata("design:type", Function),
80
86
  _ts_metadata("design:paramtypes", [
81
87
  typeof PromoCodeRepository === "undefined" ? Object : PromoCodeRepository,
82
- typeof PromoCodeRedemptionRepository === "undefined" ? Object : PromoCodeRedemptionRepository
88
+ typeof PromoCodeRedemptionRepository === "undefined" ? Object : PromoCodeRedemptionRepository,
89
+ Object
83
90
  ])
84
91
  ], PromoCodeExpirer);
85
92
 
@@ -727,6 +734,9 @@ var PromoCodesModule = class _PromoCodesModule {
727
734
  provide: PROMO_SERVICE_CONFIG_TOKEN,
728
735
  useValue: options.config ?? {}
729
736
  },
737
+ ...options.holdRepository ? [
738
+ asProvider(PROMO_CODE_HOLD_REPOSITORY_TOKEN, options.holdRepository)
739
+ ] : [],
730
740
  PromoCodesService,
731
741
  PromoCodeRateLimitGuard
732
742
  ];
@@ -1,43 +1,33 @@
1
1
  import {
2
2
  PromoCodesService,
3
3
  computeDiscountGross
4
- } from "./chunk-XWAZNKAB.js";
4
+ } from "./chunk-3RLWML2S.js";
5
5
  import {
6
6
  SubscriptionContractService,
7
7
  appendImplicitDiscountLineItem
8
- } from "./chunk-U465NU23.js";
8
+ } from "./chunk-NDLC5GYK.js";
9
9
  import {
10
10
  resolveBundlePriceNet
11
11
  } from "./chunk-KPEMTBOP.js";
12
- import {
13
- SUBSCRIPTION_CONTRACT_REPOSITORY_TOKEN
14
- } from "./chunk-VBRYPU7V.js";
15
12
  import {
16
13
  grossFromNet,
17
14
  netFromGross,
18
15
  round2
19
- } from "./chunk-QSBMFY4I.js";
16
+ } from "./chunk-6PJE7EWO.js";
20
17
  import {
21
18
  BUNDLE_REPOSITORY_TOKEN,
22
19
  CATALOG_ENTRY_REPOSITORY_TOKEN,
23
20
  PLAN_REPOSITORY_TOKEN,
24
21
  PROMOTION_REPOSITORY_TOKEN
25
- } from "./chunk-AOQJEYOL.js";
22
+ } from "./chunk-3QVGA6JX.js";
26
23
  import {
27
24
  SubscriberService,
28
- subscriberProviders
29
- } from "./chunk-U4X2YRRO.js";
30
- import {
31
25
  settleNewSubscriberDetails
32
- } from "./chunk-3HT3REXB.js";
33
- import {
34
- SaaSiCatPublicRoute
35
- } from "./chunk-YSC46BGM.js";
26
+ } from "./chunk-LF6J4YYN.js";
36
27
  import {
37
- PLAN_CATALOG_TOKEN
38
- } from "./chunk-323FB6TD.js";
28
+ PLAN_CATALOG_SETTINGS_TOKEN
29
+ } from "./chunk-FEQZCWUL.js";
39
30
  import {
40
- asProvider,
41
31
  codedError
42
32
  } from "./chunk-J7NJRK3K.js";
43
33
  import {
@@ -46,7 +36,7 @@ import {
46
36
 
47
37
  // src/checkout-offer/checkout-offer-pricing.ts
48
38
  import { Inject, Injectable, Optional, UnprocessableEntityException } from "@nestjs/common";
49
- import { CONTRACT_ERROR_CODES, applyPromo, pickActivePromo } from "@saasicat/core";
39
+ import { CONTRACT_ERROR_CODES, promotionOnPrice } from "@saasicat/core";
50
40
 
51
41
  // src/checkout-offer/bundle-version-bookable.ts
52
42
  import { startOfUtcDay } from "@saasicat/core";
@@ -95,13 +85,13 @@ var CheckoutOfferPricing = class {
95
85
  static {
96
86
  __name(this, "CheckoutOfferPricing");
97
87
  }
98
- catalog;
88
+ settings;
99
89
  plans;
100
90
  bundles;
101
91
  promotions;
102
92
  promoCodes;
103
- constructor(catalog, plans, bundles = null, promotions = null, promoCodes = null) {
104
- this.catalog = catalog;
93
+ constructor(settings, plans, bundles = null, promotions = null, promoCodes = null) {
94
+ this.settings = settings;
105
95
  this.plans = plans;
106
96
  this.bundles = bundles;
107
97
  this.promotions = promotions;
@@ -134,7 +124,9 @@ var CheckoutOfferPricing = class {
134
124
  billingCycle: offer.billingCycle,
135
125
  bundleVersionIds: offer.bundleVersionIds ?? [],
136
126
  promoCode: offer.promoCode,
137
- locale: offer.locale
127
+ locale: offer.locale,
128
+ checkoutOfferId: offer.id,
129
+ concluding: true
138
130
  };
139
131
  const pricedAt = new Date(offer.updatedAt);
140
132
  let repriced;
@@ -150,7 +142,7 @@ var CheckoutOfferPricing = class {
150
142
  if (moneyOf(repriced) !== moneyOf(offer)) throw notCurrent();
151
143
  }
152
144
  async compute(input, planVersion, bundleVersions, asOf) {
153
- const vatRate = this.catalog.vatRate;
145
+ const vatRate = this.settings.vatRate;
154
146
  const promotions = this.promotions ? await this.promotions.list() : [];
155
147
  const plan = await this.plans.findByKey(input.planKey);
156
148
  const planLine = this.pricePlan(input, planVersion, plan?.label, promotions, asOf);
@@ -167,7 +159,7 @@ var CheckoutOfferPricing = class {
167
159
  const promoCodeSnapshot = await this.pricePromoCode(input, planNetAfterPromotion, vatRate);
168
160
  const effectiveNet = Math.max(0, round2(regularNet - promotionDiscount - (promoCodeSnapshot?.resolvedAmountNet ?? 0)));
169
161
  const priceBreakdown = {
170
- currency: this.catalog.currency,
162
+ currency: this.settings.currency,
171
163
  billingCycle: input.billingCycle,
172
164
  planNet,
173
165
  bundlesNet,
@@ -279,7 +271,7 @@ var CheckoutOfferPricing = class {
279
271
  quantity: 1,
280
272
  unit: null,
281
273
  priceNet: fields.priceNet,
282
- priceGross: grossFromNet(fields.priceNet, this.catalog.vatRate),
274
+ priceGross: grossFromNet(fields.priceNet, this.settings.vatRate),
283
275
  billingCycle: fields.billingCycle,
284
276
  featuresSnapshot: [
285
277
  ...fields.features
@@ -304,6 +296,9 @@ var CheckoutOfferPricing = class {
304
296
  code: input.promoCode,
305
297
  planId: input.planKey,
306
298
  billingCycle: wireCycle(input.billingCycle)
299
+ }, {
300
+ checkoutOfferId: input.checkoutOfferId,
301
+ concluding: input.concluding
307
302
  });
308
303
  if (!preview.valid) throw promoCodeNotAccepted(preview.reason);
309
304
  const planGross = grossFromNet(planNet, vatRate);
@@ -324,7 +319,7 @@ var CheckoutOfferPricing = class {
324
319
  };
325
320
  CheckoutOfferPricing = _ts_decorate([
326
321
  Injectable(),
327
- _ts_param(0, Inject(PLAN_CATALOG_TOKEN)),
322
+ _ts_param(0, Inject(PLAN_CATALOG_SETTINGS_TOKEN)),
328
323
  _ts_param(1, Inject(PLAN_REPOSITORY_TOKEN)),
329
324
  _ts_param(2, Optional()),
330
325
  _ts_param(2, Inject(BUNDLE_REPOSITORY_TOKEN)),
@@ -334,7 +329,7 @@ CheckoutOfferPricing = _ts_decorate([
334
329
  _ts_param(4, Inject(PromoCodesService)),
335
330
  _ts_metadata("design:type", Function),
336
331
  _ts_metadata("design:paramtypes", [
337
- typeof PlanCatalog === "undefined" ? Object : PlanCatalog,
332
+ typeof PlanCatalogSettings === "undefined" ? Object : PlanCatalogSettings,
338
333
  typeof PlanRepository === "undefined" ? Object : PlanRepository,
339
334
  Object,
340
335
  Object,
@@ -342,10 +337,10 @@ CheckoutOfferPricing = _ts_decorate([
342
337
  ])
343
338
  ], CheckoutOfferPricing);
344
339
  function promotionFor(promotions, targetKey, targetType, input, priceNet, asOf) {
345
- const promotion = pickActivePromo(promotions, targetKey, input.locale, input.billingCycle, asOf, targetType);
346
- const applied = applyPromo(promotion, priceNet);
347
- if (!promotion || !applied) return null;
348
- const resolvedAmountNet = round2(Math.max(0, priceNet - applied.discounted));
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));
349
344
  if (resolvedAmountNet <= 0) return null;
350
345
  const texts = promotion.i18n?.[input.locale] ?? promotion.i18n?.[DEFAULT_LOCALE] ?? {};
351
346
  return {
@@ -389,13 +384,17 @@ function moneyOf(offer) {
389
384
  b.vatRate,
390
385
  b.effectiveGross
391
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
392
  lines: (offer.lineItems ?? []).map((item) => [
393
393
  item.kind,
394
394
  item.sourceKey,
395
395
  item.sourceVersionId ?? null,
396
396
  item.quantity,
397
397
  item.priceNet,
398
- item.priceGross,
399
398
  item.billingCycle
400
399
  ]),
401
400
  promotions: (offer.promotionSnapshots ?? []).map((p) => [
@@ -474,6 +473,7 @@ function _ts_param2(paramIndex, decorator) {
474
473
  }
475
474
  __name(_ts_param2, "_ts_param");
476
475
  var DEFAULT_OFFER_LOCALE = "de";
476
+ var HOLD_ATTEMPTS = 3;
477
477
  var CheckoutOfferService = class {
478
478
  static {
479
479
  __name(this, "CheckoutOfferService");
@@ -486,7 +486,8 @@ var CheckoutOfferService = class {
486
486
  contracts;
487
487
  transactions;
488
488
  subscribers;
489
- constructor(repo, pricing, bundles = null, plans = null, catalogEntries = null, contracts = null, transactions = null, subscribers = null) {
489
+ promoCodes;
490
+ constructor(repo, pricing, bundles = null, plans = null, catalogEntries = null, contracts = null, transactions = null, subscribers = null, promoCodes = null) {
490
491
  this.repo = repo;
491
492
  this.pricing = pricing;
492
493
  this.bundles = bundles;
@@ -495,6 +496,7 @@ var CheckoutOfferService = class {
495
496
  this.contracts = contracts;
496
497
  this.transactions = transactions;
497
498
  this.subscribers = subscribers;
499
+ this.promoCodes = promoCodes;
498
500
  }
499
501
  list(filter) {
500
502
  return this.repo.list(filter);
@@ -538,7 +540,9 @@ var CheckoutOfferService = class {
538
540
  }
539
541
  /**
540
542
  * Customization in onboarding — only while the offer is `open`. The plan
541
- * stays; whatever else changes, the whole offer is priced again.
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.
542
546
  */
543
547
  async update(id, change) {
544
548
  const existing = await this.getById(id);
@@ -550,7 +554,8 @@ var CheckoutOfferService = class {
550
554
  billingCycle,
551
555
  bundleVersionIds: change.bundleVersionIds ?? existing.bundleVersionIds ?? [],
552
556
  promoCode: change.promoCode !== void 0 ? change.promoCode : existing.promoCode,
553
- locale
557
+ locale,
558
+ checkoutOfferId: id
554
559
  });
555
560
  await this.assertFeatureRequiresSatisfied({
556
561
  planKey: existing.planKey,
@@ -558,7 +563,8 @@ var CheckoutOfferService = class {
558
563
  bundleVersionIds: priced.bundleVersionIds,
559
564
  lineItems: priced.lineItems
560
565
  });
561
- return this.repo.update(id, {
566
+ const codeChanges = priced.promoCode !== existing.promoCode;
567
+ const updated = await this.repo.update(id, {
562
568
  billingCycle,
563
569
  locale,
564
570
  ...change.validUntil !== void 0 ? {
@@ -566,6 +572,55 @@ var CheckoutOfferService = class {
566
572
  } : {},
567
573
  ...priced
568
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);
569
624
  }
570
625
  /**
571
626
  * Freezes the offer (`status = 'consumed'`). Returns the final
@@ -620,6 +675,12 @@ var CheckoutOfferService = class {
620
675
  * where a refusal still undoes everything. Nothing checks the code again
621
676
  * after `within`, so a redemption that takes its last slot does not refuse
622
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.
623
684
  */
624
685
  async conclude(id, options, within) {
625
686
  const { contracts, transactions, subscribers } = this.conclusionPorts();
@@ -640,7 +701,7 @@ var CheckoutOfferService = class {
640
701
  consumeFailed = true;
641
702
  throw error;
642
703
  }
643
- const data = contracts.prepareFromOffer(offer, contractOptions);
704
+ const data = contracts.createDataFromOffer(offer, contractOptions);
644
705
  if (!isDeepStrictEqual(data, checked)) throw offerChanged(id);
645
706
  if (subscriber) await subscribers.createForTenant(tenantId, subscriber, tx);
646
707
  const contract = await contracts.create(data, tx);
@@ -648,7 +709,9 @@ var CheckoutOfferService = class {
648
709
  offer,
649
710
  contract
650
711
  };
712
+ const handedOver = await this.promoCodes?.handOverCheckoutHold(id, tx) ?? false;
651
713
  if (within) await within(tx, concluded);
714
+ if (handedOver) await this.promoCodes?.releaseCheckoutHold(id, tx);
652
715
  return concluded;
653
716
  }, "concludeOn");
654
717
  try {
@@ -849,6 +912,8 @@ CheckoutOfferService = _ts_decorate2([
849
912
  _ts_param2(6, Inject2(CHECKOUT_OFFER_TRANSACTION_RUNNER_TOKEN)),
850
913
  _ts_param2(7, Optional2()),
851
914
  _ts_param2(7, Inject2(SubscriberService)),
915
+ _ts_param2(8, Optional2()),
916
+ _ts_param2(8, Inject2(PromoCodesService)),
852
917
  _ts_metadata2("design:type", Function),
853
918
  _ts_metadata2("design:paramtypes", [
854
919
  typeof CheckoutOfferRepository === "undefined" ? Object : CheckoutOfferRepository,
@@ -858,13 +923,14 @@ CheckoutOfferService = _ts_decorate2([
858
923
  Object,
859
924
  Object,
860
925
  Object,
926
+ Object,
861
927
  Object
862
928
  ])
863
929
  ], CheckoutOfferService);
864
- function offerChanged(offerId) {
930
+ function offerChanged(offerId, during = "it was being concluded") {
865
931
  return new ConflictException({
866
932
  code: CONTRACT_ERROR_CODES2.CHECKOUT_OFFER_CHANGED,
867
- message: `Checkout offer '${offerId}' changed while it was being concluded. Load it again.`,
933
+ message: `Checkout offer '${offerId}' changed while ${during}. Load it again.`,
868
934
  params: {
869
935
  offerId
870
936
  }
@@ -872,271 +938,9 @@ function offerChanged(offerId) {
872
938
  }
873
939
  __name(offerChanged, "offerChanged");
874
940
 
875
- // src/checkout-offer/dto/checkout-offer.dto.ts
876
- import { ArrayMaxSize, IsArray, IsDateString, IsIn, IsOptional, IsString, MaxLength, ValidateIf } from "class-validator";
877
- function _ts_decorate3(decorators, target, key, desc) {
878
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
879
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
880
- 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;
881
- return c > 3 && r && Object.defineProperty(target, key, r), r;
882
- }
883
- __name(_ts_decorate3, "_ts_decorate");
884
- function _ts_metadata3(k, v) {
885
- if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
886
- }
887
- __name(_ts_metadata3, "_ts_metadata");
888
- var CYCLES = [
889
- "monthly",
890
- "yearly"
891
- ];
892
- var MAX_BUNDLE_VERSIONS = 50;
893
- var CreateCheckoutOfferDto = class {
894
- static {
895
- __name(this, "CreateCheckoutOfferDto");
896
- }
897
- planKey;
898
- billingCycle;
899
- bundleVersionIds;
900
- promoCode;
901
- locale;
902
- validUntil;
903
- };
904
- _ts_decorate3([
905
- IsString(),
906
- MaxLength(64),
907
- _ts_metadata3("design:type", String)
908
- ], CreateCheckoutOfferDto.prototype, "planKey", void 0);
909
- _ts_decorate3([
910
- IsString(),
911
- IsIn(CYCLES),
912
- _ts_metadata3("design:type", String)
913
- ], CreateCheckoutOfferDto.prototype, "billingCycle", void 0);
914
- _ts_decorate3([
915
- IsOptional(),
916
- IsArray(),
917
- ArrayMaxSize(MAX_BUNDLE_VERSIONS),
918
- IsString({
919
- each: true
920
- }),
921
- _ts_metadata3("design:type", Array)
922
- ], CreateCheckoutOfferDto.prototype, "bundleVersionIds", void 0);
923
- _ts_decorate3([
924
- IsOptional(),
925
- ValidateIf((_o, value) => value !== null),
926
- IsString(),
927
- MaxLength(64),
928
- _ts_metadata3("design:type", Object)
929
- ], CreateCheckoutOfferDto.prototype, "promoCode", void 0);
930
- _ts_decorate3([
931
- IsOptional(),
932
- IsString(),
933
- MaxLength(8),
934
- _ts_metadata3("design:type", String)
935
- ], CreateCheckoutOfferDto.prototype, "locale", void 0);
936
- _ts_decorate3([
937
- IsOptional(),
938
- ValidateIf((_o, value) => value !== null),
939
- IsDateString(),
940
- _ts_metadata3("design:type", Object)
941
- ], CreateCheckoutOfferDto.prototype, "validUntil", void 0);
942
- var UpdateCheckoutOfferDto = class {
943
- static {
944
- __name(this, "UpdateCheckoutOfferDto");
945
- }
946
- billingCycle;
947
- bundleVersionIds;
948
- promoCode;
949
- locale;
950
- validUntil;
951
- };
952
- _ts_decorate3([
953
- IsOptional(),
954
- IsString(),
955
- IsIn(CYCLES),
956
- _ts_metadata3("design:type", String)
957
- ], UpdateCheckoutOfferDto.prototype, "billingCycle", void 0);
958
- _ts_decorate3([
959
- IsOptional(),
960
- IsArray(),
961
- ArrayMaxSize(MAX_BUNDLE_VERSIONS),
962
- IsString({
963
- each: true
964
- }),
965
- _ts_metadata3("design:type", Array)
966
- ], UpdateCheckoutOfferDto.prototype, "bundleVersionIds", void 0);
967
- _ts_decorate3([
968
- IsOptional(),
969
- ValidateIf((_o, value) => value !== null),
970
- IsString(),
971
- MaxLength(64),
972
- _ts_metadata3("design:type", Object)
973
- ], UpdateCheckoutOfferDto.prototype, "promoCode", void 0);
974
- _ts_decorate3([
975
- IsOptional(),
976
- IsString(),
977
- MaxLength(8),
978
- _ts_metadata3("design:type", String)
979
- ], UpdateCheckoutOfferDto.prototype, "locale", void 0);
980
- _ts_decorate3([
981
- IsOptional(),
982
- ValidateIf((_o, value) => value !== null),
983
- IsDateString(),
984
- _ts_metadata3("design:type", Object)
985
- ], UpdateCheckoutOfferDto.prototype, "validUntil", void 0);
986
-
987
- // src/checkout-offer/checkout-offer.controller.ts
988
- import { Body, Controller, Get, Inject as Inject3, Param, ParseUUIDPipe, Patch, Post, UseGuards } from "@nestjs/common";
989
- function _ts_decorate4(decorators, target, key, desc) {
990
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
991
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
992
- 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;
993
- return c > 3 && r && Object.defineProperty(target, key, r), r;
994
- }
995
- __name(_ts_decorate4, "_ts_decorate");
996
- function _ts_metadata4(k, v) {
997
- if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
998
- }
999
- __name(_ts_metadata4, "_ts_metadata");
1000
- function _ts_param3(paramIndex, decorator) {
1001
- return function(target, key) {
1002
- decorator(target, key, paramIndex);
1003
- };
1004
- }
1005
- __name(_ts_param3, "_ts_param");
1006
- function buildCheckoutOfferController(guards) {
1007
- let GeneratedCheckoutOfferController = class GeneratedCheckoutOfferController {
1008
- static {
1009
- __name(this, "GeneratedCheckoutOfferController");
1010
- }
1011
- service;
1012
- constructor(service) {
1013
- this.service = service;
1014
- }
1015
- /** The website creates an offer when a package is clicked. */
1016
- create(dto) {
1017
- return this.service.create(dto);
1018
- }
1019
- /** Onboarding reads the pre-selected offer (`?offer=<id>`). */
1020
- getById(id) {
1021
- return this.service.getById(id);
1022
- }
1023
- /** Onboarding customization — as long as the offer is `open`. */
1024
- update(id, dto) {
1025
- return this.service.update(id, dto);
1026
- }
1027
- };
1028
- _ts_decorate4([
1029
- Post(),
1030
- _ts_param3(0, Body()),
1031
- _ts_metadata4("design:type", Function),
1032
- _ts_metadata4("design:paramtypes", [
1033
- typeof CreateCheckoutOfferDto === "undefined" ? Object : CreateCheckoutOfferDto
1034
- ]),
1035
- _ts_metadata4("design:returntype", void 0)
1036
- ], GeneratedCheckoutOfferController.prototype, "create", null);
1037
- _ts_decorate4([
1038
- Get(":id"),
1039
- _ts_param3(0, Param("id", new ParseUUIDPipe())),
1040
- _ts_metadata4("design:type", Function),
1041
- _ts_metadata4("design:paramtypes", [
1042
- String
1043
- ]),
1044
- _ts_metadata4("design:returntype", void 0)
1045
- ], GeneratedCheckoutOfferController.prototype, "getById", null);
1046
- _ts_decorate4([
1047
- Patch(":id"),
1048
- _ts_param3(0, Param("id", new ParseUUIDPipe())),
1049
- _ts_param3(1, Body()),
1050
- _ts_metadata4("design:type", Function),
1051
- _ts_metadata4("design:paramtypes", [
1052
- String,
1053
- typeof UpdateCheckoutOfferDto === "undefined" ? Object : UpdateCheckoutOfferDto
1054
- ]),
1055
- _ts_metadata4("design:returntype", void 0)
1056
- ], GeneratedCheckoutOfferController.prototype, "update", null);
1057
- GeneratedCheckoutOfferController = _ts_decorate4([
1058
- Controller("public/checkout-offer"),
1059
- SaaSiCatPublicRoute(),
1060
- UseGuards(...guards),
1061
- _ts_param3(0, Inject3(CheckoutOfferService)),
1062
- _ts_metadata4("design:type", Function),
1063
- _ts_metadata4("design:paramtypes", [
1064
- typeof CheckoutOfferService === "undefined" ? Object : CheckoutOfferService
1065
- ])
1066
- ], GeneratedCheckoutOfferController);
1067
- return GeneratedCheckoutOfferController;
1068
- }
1069
- __name(buildCheckoutOfferController, "buildCheckoutOfferController");
1070
-
1071
- // src/checkout-offer/checkout-offer.module.ts
1072
- import { Module } from "@nestjs/common";
1073
- function _ts_decorate5(decorators, target, key, desc) {
1074
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1075
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1076
- 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;
1077
- return c > 3 && r && Object.defineProperty(target, key, r), r;
1078
- }
1079
- __name(_ts_decorate5, "_ts_decorate");
1080
- var CheckoutOfferModule = class _CheckoutOfferModule {
1081
- static {
1082
- __name(this, "CheckoutOfferModule");
1083
- }
1084
- static forRoot(options) {
1085
- if (!options.planRepository) {
1086
- throw new Error("CheckoutOfferModule: `planRepository` is required \u2014 every offer is priced from the plan version on sale, and an offer without it would have to take its price from the request.");
1087
- }
1088
- if (options.conclusion && (!options.conclusion.subscriptionContractRepository || !options.conclusion.subscriberRepository || !options.conclusion.transactionRunner)) {
1089
- throw new Error("CheckoutOfferModule: `conclusion` needs `subscriptionContractRepository`, `subscriberRepository` and `transactionRunner` \u2014 concluding an offer writes the contract with its party and consumes the offer in one transaction, and without any of them it would do only part of that.");
1090
- }
1091
- const controllers = [];
1092
- if (options.controller) {
1093
- controllers.push(buildCheckoutOfferController(options.controller.guards));
1094
- }
1095
- return {
1096
- module: _CheckoutOfferModule,
1097
- global: options.global ?? false,
1098
- imports: options.imports ?? [],
1099
- controllers,
1100
- providers: [
1101
- asProvider(CHECKOUT_OFFER_REPOSITORY_TOKEN, options.checkoutOfferRepository),
1102
- ...options.bundleRepository ? [
1103
- asProvider(BUNDLE_REPOSITORY_TOKEN, options.bundleRepository)
1104
- ] : [],
1105
- asProvider(PLAN_REPOSITORY_TOKEN, options.planRepository),
1106
- ...options.promotionRepository ? [
1107
- asProvider(PROMOTION_REPOSITORY_TOKEN, options.promotionRepository)
1108
- ] : [],
1109
- ...options.catalogEntryRepository ? [
1110
- asProvider(CATALOG_ENTRY_REPOSITORY_TOKEN, options.catalogEntryRepository)
1111
- ] : [],
1112
- ...options.conclusion ? [
1113
- asProvider(SUBSCRIPTION_CONTRACT_REPOSITORY_TOKEN, options.conclusion.subscriptionContractRepository),
1114
- asProvider(CHECKOUT_OFFER_TRANSACTION_RUNNER_TOKEN, options.conclusion.transactionRunner),
1115
- ...subscriberProviders(options.conclusion.subscriberRepository),
1116
- SubscriptionContractService
1117
- ] : [],
1118
- CheckoutOfferPricing,
1119
- CheckoutOfferService,
1120
- ...options.extraProviders ?? []
1121
- ],
1122
- exports: [
1123
- CheckoutOfferService,
1124
- CHECKOUT_OFFER_REPOSITORY_TOKEN
1125
- ]
1126
- };
1127
- }
1128
- };
1129
- CheckoutOfferModule = _ts_decorate5([
1130
- Module({})
1131
- ], CheckoutOfferModule);
1132
-
1133
941
  export {
1134
942
  CheckoutOfferPricing,
1135
943
  CHECKOUT_OFFER_REPOSITORY_TOKEN,
1136
944
  CHECKOUT_OFFER_TRANSACTION_RUNNER_TOKEN,
1137
- CheckoutOfferService,
1138
- CreateCheckoutOfferDto,
1139
- UpdateCheckoutOfferDto,
1140
- buildCheckoutOfferController,
1141
- CheckoutOfferModule
945
+ CheckoutOfferService
1142
946
  };