@saasicat/nest 1.0.0-rc.14 → 1.0.0-rc.15

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 (46) hide show
  1. package/dist/.build-stamp +1 -1
  2. package/dist/_entries.cjs +161 -5
  3. package/dist/billing/index.d.cts +1 -1
  4. package/dist/billing/index.d.ts +1 -1
  5. package/dist/billing/index.js +10 -11
  6. package/dist/catalog/index.js +6 -6
  7. package/dist/checkout-offer/index.d.cts +9 -3
  8. package/dist/checkout-offer/index.d.ts +9 -3
  9. package/dist/checkout-offer/index.js +7 -4
  10. package/dist/{checkout-offer.module-BVXEDXex.d.ts → checkout-offer.module-B-DF2I_-.d.ts} +15 -2
  11. package/dist/{checkout-offer.module-B1O0H8EQ.d.cts → checkout-offer.module-BOX9yAWq.d.cts} +15 -2
  12. package/dist/{checkout-offer.service-BIGkcWNe.d.ts → checkout-offer.service-BZumQOmZ.d.ts} +54 -3
  13. package/dist/{checkout-offer.service-C5-YWUrp.d.cts → checkout-offer.service-Dknxgj6W.d.cts} +54 -3
  14. package/dist/{chunk-KOZYQS53.js → chunk-2TACP6MQ.js} +2 -2
  15. package/dist/{chunk-FKAWM74T.js → chunk-45B4OQOO.js} +1 -1
  16. package/dist/{chunk-O4M5HC2B.js → chunk-62WEP34A.js} +20 -20
  17. package/dist/{chunk-PEOAEM2O.js → chunk-6NGMKJNI.js} +1 -1
  18. package/dist/{chunk-UO55F6JP.js → chunk-CJZPT7CR.js} +4 -4
  19. package/dist/{chunk-L7PTXFJE.js → chunk-H3GVIQVK.js} +138 -13
  20. package/dist/{chunk-E2GFA4J2.js → chunk-LEZ6KYQ7.js} +106 -5
  21. package/dist/{chunk-GORG6MWO.js → chunk-NQVW2R7V.js} +33 -24
  22. package/dist/{define-saasicat-DBJn20Sg.d.cts → define-saasicat-Bsa6GZKE.d.cts} +1 -1
  23. package/dist/{define-saasicat-kg0AC-pQ.d.ts → define-saasicat-OWvE7lJ1.d.ts} +1 -1
  24. package/dist/{enforce-quota.interceptor-W4tYHm_k.d.ts → enforce-quota.interceptor-C5kUQYit.d.ts} +1 -1
  25. package/dist/{enforce-quota.interceptor-D8CHDs6U.d.cts → enforce-quota.interceptor-Cz5z36qf.d.cts} +1 -1
  26. package/dist/entitlement/index.js +5 -5
  27. package/dist/index.d.cts +7 -7
  28. package/dist/index.d.ts +7 -7
  29. package/dist/index.js +104 -103
  30. package/dist/{module-options-zdnvldbv.d.ts → module-options-CCP890cz.d.ts} +7 -2
  31. package/dist/{module-options-BnLp7el-.d.cts → module-options-Cq0Lv2i0.d.cts} +7 -2
  32. package/dist/platform/index.d.cts +8 -8
  33. package/dist/platform/index.d.ts +8 -8
  34. package/dist/platform/index.js +34 -35
  35. package/dist/subscription-contract/index.d.cts +1 -1
  36. package/dist/subscription-contract/index.d.ts +1 -1
  37. package/dist/subscription-contract/index.js +2 -3
  38. package/dist/{subscription-contract.service-hD87MKyg.d.cts → subscription-contract.service-wMqIPFkS.d.cts} +13 -2
  39. package/dist/{subscription-contract.service-hD87MKyg.d.ts → subscription-contract.service-wMqIPFkS.d.ts} +13 -2
  40. package/dist/testing/index.d.cts +5 -4
  41. package/dist/testing/index.d.ts +5 -4
  42. package/dist/testing/index.js +21 -16
  43. package/package.json +3 -3
  44. package/dist/chunk-DR2SQNMX.js +0 -91
  45. package/dist/{chunk-AFZQ7GTQ.js → chunk-GYI3E2YY.js} +9 -9
  46. package/dist/{chunk-HRUVUY5E.js → chunk-JMV6J7YP.js} +9 -9
package/dist/.build-stamp CHANGED
@@ -1 +1 @@
1
- d16cb942d700188830a8fcabcf7358a1569dbb615945cef61e725d328e2b63bb
1
+ 5b5c697fcb7f0f396443441223db4c63896950ff67b89351b80f193bfc738b7d
package/dist/_entries.cjs CHANGED
@@ -4962,9 +4962,14 @@ var SubscriptionContractService = class {
4962
4962
  }
4963
4963
  return subscriptionContractToInvoiceSnapshot(contract);
4964
4964
  }
4965
- async create(data) {
4965
+ /** With `tx`, the contract is written on that transaction and undone with it. */
4966
+ async create(data, tx) {
4966
4967
  this.assertCreateData(data);
4967
- return this.repo.create(this.cloneCreateData(data));
4968
+ return this.repo.create(this.cloneCreateData(data), tx);
4969
+ }
4970
+ /** The contract concluded from a checkout offer, or `null` when none was. */
4971
+ findByOriginalOfferId(offerId, tx) {
4972
+ return this.repo.findByOriginalOfferId(offerId, tx);
4968
4973
  }
4969
4974
  async terminate(contractId, data) {
4970
4975
  const existing = await this.getById(contractId);
@@ -5002,6 +5007,20 @@ var SubscriptionContractService = class {
5002
5007
  }
5003
5008
  });
5004
5009
  }
5010
+ return this.dataFromOffer(offer, options);
5011
+ }
5012
+ /**
5013
+ * The contract an offer becomes, checked the way `create` checks it, before
5014
+ * anything is written. Unlike `createDataFromOffer` it takes an offer that
5015
+ * is still open: concluding an offer refuses a contract it could not create
5016
+ * before the offer is consumed, rather than after.
5017
+ */
5018
+ prepareFromOffer(offer, options) {
5019
+ const data = this.dataFromOffer(offer, options);
5020
+ this.assertCreateData(data);
5021
+ return data;
5022
+ }
5023
+ dataFromOffer(offer, options) {
5005
5024
  const lineItems = this.lineItemsFromOffer(offer);
5006
5025
  return {
5007
5026
  tenantId: options.tenantId,
@@ -13933,6 +13952,7 @@ __name(runSeedGateFromFile, "runSeedGateFromFile");
13933
13952
  var checkout_offer_exports = {};
13934
13953
  __export(checkout_offer_exports, {
13935
13954
  CHECKOUT_OFFER_REPOSITORY_TOKEN: () => CHECKOUT_OFFER_REPOSITORY_TOKEN,
13955
+ CHECKOUT_OFFER_TRANSACTION_RUNNER_TOKEN: () => CHECKOUT_OFFER_TRANSACTION_RUNNER_TOKEN,
13936
13956
  CheckoutOfferModule: () => CheckoutOfferModule,
13937
13957
  CheckoutOfferPricing: () => CheckoutOfferPricing,
13938
13958
  CheckoutOfferService: () => CheckoutOfferService,
@@ -13942,6 +13962,7 @@ __export(checkout_offer_exports, {
13942
13962
  });
13943
13963
 
13944
13964
  // src/checkout-offer/checkout-offer.service.ts
13965
+ var import_node_util = require("util");
13945
13966
  var import_common63 = require("@nestjs/common");
13946
13967
  var import_core40 = require("@saasicat/core");
13947
13968
 
@@ -14349,6 +14370,7 @@ __name(promoCodeNotAccepted, "promoCodeNotAccepted");
14349
14370
 
14350
14371
  // src/checkout-offer/checkout-offer.tokens.ts
14351
14372
  var CHECKOUT_OFFER_REPOSITORY_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/nest/CheckoutOfferRepository");
14373
+ var CHECKOUT_OFFER_TRANSACTION_RUNNER_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/nest/CheckoutOfferTransactionRunner");
14352
14374
 
14353
14375
  // src/checkout-offer/checkout-offer.service.ts
14354
14376
  function _ts_decorate67(decorators, target, key, desc) {
@@ -14378,12 +14400,16 @@ var CheckoutOfferService = class {
14378
14400
  bundles;
14379
14401
  plans;
14380
14402
  catalogEntries;
14381
- constructor(repo, pricing, bundles = null, plans = null, catalogEntries = null) {
14403
+ contracts;
14404
+ transactions;
14405
+ constructor(repo, pricing, bundles = null, plans = null, catalogEntries = null, contracts = null, transactions = null) {
14382
14406
  this.repo = repo;
14383
14407
  this.pricing = pricing;
14384
14408
  this.bundles = bundles;
14385
14409
  this.plans = plans;
14386
14410
  this.catalogEntries = catalogEntries;
14411
+ this.contracts = contracts;
14412
+ this.transactions = transactions;
14387
14413
  }
14388
14414
  list(filter) {
14389
14415
  return this.repo.list(filter);
@@ -14473,11 +14499,101 @@ var CheckoutOfferService = class {
14473
14499
  * is kept for amounts that no longer match.
14474
14500
  */
14475
14501
  async consume(id) {
14502
+ await this.assertConsumable(id);
14503
+ return this.repo.consume(id);
14504
+ }
14505
+ /**
14506
+ * Consumes an offer and creates its contract in one transaction, together
14507
+ * with the application's own writes in `within`, so an offer is never
14508
+ * consumed without its contract, and no contract is written for an offer
14509
+ * that stays open.
14510
+ *
14511
+ * Everything that can refuse runs before the transaction: what `consume`
14512
+ * checks, and the checks the contract the offer becomes is held to. Inside
14513
+ * it, the offer is consumed on the condition that it is still open, the
14514
+ * contract is written, and `within` runs; an error in any of them undoes
14515
+ * the others, and the offer can be concluded again.
14516
+ *
14517
+ * An offer that is already concluded for the same tenant answers with its
14518
+ * contract rather than a refusal, so a caller retrying after a lost answer,
14519
+ * or losing a race to another call for that tenant, gets the conclusion that
14520
+ * stands; `within` does not run again, its writes committed with it. A
14521
+ * conclusion for another tenant is refused with
14522
+ * `CHECKOUT_OFFER_ALREADY_CONSUMED`, and an offer changed between the checks
14523
+ * and the transaction with `CHECKOUT_OFFER_CHANGED`, nothing written.
14524
+ *
14525
+ * A promo code on the offer is redeemed in `within`, with
14526
+ * `PromoCodesService.redeemInTransaction` on the same transaction. Before
14527
+ * the transaction the code is only checked the way pricing checks it; the
14528
+ * redemption checks the rest, the customer and the remaining redemptions,
14529
+ * where a refusal still undoes everything. Nothing checks the code again
14530
+ * after `within`, so a redemption that takes its last slot does not refuse
14531
+ * the offer it was redeemed for.
14532
+ */
14533
+ async conclude(id, options, within) {
14534
+ const { contracts, transactions } = this.conclusionPorts();
14535
+ const { tenantId } = options;
14536
+ const standing = await this.standingConclusion(await this.getById(id), contracts, tenantId);
14537
+ if (standing) return standing;
14538
+ const existing = await this.assertConsumable(id);
14539
+ const checked = contracts.prepareFromOffer(existing, options);
14540
+ let consumeFailed = false;
14541
+ try {
14542
+ return await transactions.run(async (tx) => {
14543
+ consumeFailed = false;
14544
+ let offer;
14545
+ try {
14546
+ offer = await this.repo.consume(id, tx);
14547
+ } catch (error) {
14548
+ consumeFailed = true;
14549
+ throw error;
14550
+ }
14551
+ const data = contracts.prepareFromOffer(offer, options);
14552
+ if (!(0, import_node_util.isDeepStrictEqual)(data, checked)) throw offerChanged(id);
14553
+ const contract = await contracts.create(data, tx);
14554
+ const concluded = {
14555
+ offer,
14556
+ contract
14557
+ };
14558
+ if (within) await within(tx, concluded);
14559
+ return concluded;
14560
+ });
14561
+ } catch (error) {
14562
+ if (!consumeFailed) throw error;
14563
+ const offer = await this.repo.findById(id).catch(() => null);
14564
+ const concludedMeanwhile = await this.standingConclusion(offer, contracts, tenantId).catch(() => null);
14565
+ if (concludedMeanwhile) return concludedMeanwhile;
14566
+ throw error;
14567
+ }
14568
+ }
14569
+ async assertConsumable(id) {
14476
14570
  const existing = await this.getById(id);
14477
14571
  this.assertOpen(existing, "consumed");
14478
14572
  await this.assertBundleVersionsStillBookable(existing);
14479
14573
  await this.pricing.assertPricedByCatalogue(existing);
14480
- return this.repo.consume(id);
14574
+ return existing;
14575
+ }
14576
+ /**
14577
+ * The conclusion that stands for this tenant, if the offer has one. An offer
14578
+ * carries no tenant and its id travels in a link, so a conclusion for
14579
+ * another tenant is not an answer: that caller is refused as before.
14580
+ */
14581
+ async standingConclusion(offer, contracts, tenantId) {
14582
+ if (!offer || offer.status !== "consumed") return null;
14583
+ const contract = await contracts.findByOriginalOfferId(offer.id);
14584
+ return contract && contract.tenantId === tenantId ? {
14585
+ offer,
14586
+ contract
14587
+ } : null;
14588
+ }
14589
+ conclusionPorts() {
14590
+ if (!this.contracts || !this.transactions) {
14591
+ throw new Error("CheckoutOfferService.conclude needs the subscription contract repository and a transaction runner: pass `conclusion` to CheckoutOfferModule.forRoot, or give SaaSiCatModule.forRoot a persistence bundle that has both.");
14592
+ }
14593
+ return {
14594
+ contracts: this.contracts,
14595
+ transactions: this.transactions
14596
+ };
14481
14597
  }
14482
14598
  /**
14483
14599
  * #35 P6 — server-side requires validation: the dependencies of all
@@ -14615,15 +14731,31 @@ CheckoutOfferService = _ts_decorate67([
14615
14731
  _ts_param45(3, (0, import_common63.Inject)(PLAN_REPOSITORY_TOKEN)),
14616
14732
  _ts_param45(4, (0, import_common63.Optional)()),
14617
14733
  _ts_param45(4, (0, import_common63.Inject)(CATALOG_ENTRY_REPOSITORY_TOKEN)),
14734
+ _ts_param45(5, (0, import_common63.Optional)()),
14735
+ _ts_param45(5, (0, import_common63.Inject)(SubscriptionContractService)),
14736
+ _ts_param45(6, (0, import_common63.Optional)()),
14737
+ _ts_param45(6, (0, import_common63.Inject)(CHECKOUT_OFFER_TRANSACTION_RUNNER_TOKEN)),
14618
14738
  _ts_metadata55("design:type", Function),
14619
14739
  _ts_metadata55("design:paramtypes", [
14620
14740
  typeof CheckoutOfferRepository === "undefined" ? Object : CheckoutOfferRepository,
14621
14741
  typeof CheckoutOfferPricing === "undefined" ? Object : CheckoutOfferPricing,
14622
14742
  Object,
14623
14743
  Object,
14744
+ Object,
14745
+ Object,
14624
14746
  Object
14625
14747
  ])
14626
14748
  ], CheckoutOfferService);
14749
+ function offerChanged(offerId) {
14750
+ return new import_common63.ConflictException({
14751
+ code: import_core40.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_CHANGED,
14752
+ message: `Checkout offer '${offerId}' changed while it was being concluded. Load it again.`,
14753
+ params: {
14754
+ offerId
14755
+ }
14756
+ });
14757
+ }
14758
+ __name(offerChanged, "offerChanged");
14627
14759
 
14628
14760
  // src/checkout-offer/checkout-offer.controller.ts
14629
14761
  var import_common64 = require("@nestjs/common");
@@ -14840,6 +14972,9 @@ var CheckoutOfferModule = class _CheckoutOfferModule {
14840
14972
  if (!options.planRepository) {
14841
14973
  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.");
14842
14974
  }
14975
+ if (options.conclusion && (!options.conclusion.subscriptionContractRepository || !options.conclusion.transactionRunner)) {
14976
+ throw new Error("CheckoutOfferModule: `conclusion` needs both `subscriptionContractRepository` and `transactionRunner` \u2014 concluding an offer writes the contract and consumes the offer in one transaction, and without either it would do only half.");
14977
+ }
14843
14978
  const controllers = [];
14844
14979
  if (options.controller) {
14845
14980
  controllers.push(buildCheckoutOfferController(options.controller.guards));
@@ -14861,6 +14996,11 @@ var CheckoutOfferModule = class _CheckoutOfferModule {
14861
14996
  ...options.catalogEntryRepository ? [
14862
14997
  asProvider(CATALOG_ENTRY_REPOSITORY_TOKEN, options.catalogEntryRepository)
14863
14998
  ] : [],
14999
+ ...options.conclusion ? [
15000
+ asProvider(SUBSCRIPTION_CONTRACT_REPOSITORY_TOKEN, options.conclusion.subscriptionContractRepository),
15001
+ asProvider(CHECKOUT_OFFER_TRANSACTION_RUNNER_TOKEN, options.conclusion.transactionRunner),
15002
+ SubscriptionContractService
15003
+ ] : [],
14864
15004
  CheckoutOfferPricing,
14865
15005
  CheckoutOfferService,
14866
15006
  ...options.extraProviders ?? []
@@ -16122,9 +16262,11 @@ function composeAdminStats({ options, persistence }) {
16122
16262
  ];
16123
16263
  }
16124
16264
  __name(composeAdminStats, "composeAdminStats");
16125
- function composeCheckoutOffer({ options, persistence }) {
16265
+ function composeCheckoutOffer({ options, adapters, persistence }) {
16126
16266
  const config = options.checkoutOffer;
16127
16267
  if (!config) return [];
16268
+ const contractRepository = config.conclusion?.subscriptionContractRepository ?? persistence?.entitlement?.subscriptionContractRepository;
16269
+ const transactionRunner = config.conclusion?.transactionRunner ?? adapters.transactionRunner;
16128
16270
  return [
16129
16271
  CheckoutOfferModule.forRoot({
16130
16272
  ...config,
@@ -16132,6 +16274,13 @@ function composeCheckoutOffer({ options, persistence }) {
16132
16274
  planRepository: config.planRepository ?? persistence?.catalog?.planRepository,
16133
16275
  promotionRepository: config.promotionRepository ?? persistence?.catalog?.promotionRepository,
16134
16276
  catalogEntryRepository: config.catalogEntryRepository ?? persistence?.catalog?.catalogEntryRepository,
16277
+ // Named by hand, it is passed on even when half of it resolves to
16278
+ // nothing, so the module refuses to start the same way it does when
16279
+ // wired directly; derived from the bundle, it is wired when both exist.
16280
+ conclusion: config.conclusion || contractRepository && transactionRunner ? {
16281
+ subscriptionContractRepository: contractRepository,
16282
+ transactionRunner
16283
+ } : void 0,
16135
16284
  imports: config.imports ?? options.imports
16136
16285
  })
16137
16286
  ];
@@ -20098,6 +20247,7 @@ __export(src_exports, {
20098
20247
  CATALOG_FEATURE_UI_REGISTRY_TOKEN: () => CATALOG_FEATURE_UI_REGISTRY_TOKEN,
20099
20248
  CATALOG_SERVICE_CONFIG_TOKEN: () => CATALOG_SERVICE_CONFIG_TOKEN,
20100
20249
  CHECKOUT_OFFER_REPOSITORY_TOKEN: () => CHECKOUT_OFFER_REPOSITORY_TOKEN,
20250
+ CHECKOUT_OFFER_TRANSACTION_RUNNER_TOKEN: () => CHECKOUT_OFFER_TRANSACTION_RUNNER_TOKEN,
20101
20251
  CODE_MAX_LENGTH: () => CODE_MAX_LENGTH,
20102
20252
  CODE_MIN_LENGTH: () => CODE_MIN_LENGTH,
20103
20253
  CODE_PATTERN: () => CODE_PATTERN,
@@ -20615,6 +20765,12 @@ var FakeSubscriptionContractRepository = class {
20615
20765
  ].filter((row) => row.tenantId === tenantId).filter((row) => this.isActiveAt(row, asOf)).sort((a, b) => b.effectiveFrom.getTime() - a.effectiveFrom.getTime())[0];
20616
20766
  return active ? this.cloneRecord(active) : null;
20617
20767
  }
20768
+ async findByOriginalOfferId(offerId) {
20769
+ const first = [
20770
+ ...this.byId.values()
20771
+ ].find((row) => row.originalOfferId === offerId);
20772
+ return first ? this.cloneRecord(first) : null;
20773
+ }
20618
20774
  async create(data) {
20619
20775
  const now = /* @__PURE__ */ new Date();
20620
20776
  const id = `contract-${this.nextContractId++}`;
@@ -9,7 +9,7 @@ export { Type as NestType } from '@nestjs/common';
9
9
  import { Reflector, BaseExceptionFilter } from '@nestjs/core';
10
10
  import { c as EntitlementService } from '../aggregation-BHo7a_In.cjs';
11
11
  import { P as ProviderSpec } from '../di-CcNeq9v-.cjs';
12
- import { S as SubscriptionContractService } from '../subscription-contract.service-hD87MKyg.cjs';
12
+ import { S as SubscriptionContractService } from '../subscription-contract.service-wMqIPFkS.cjs';
13
13
  import '../promo.service-CBMm9RU7.cjs';
14
14
  import '../admin-audit.service-4P3Djdxk.cjs';
15
15
  import '../contract-line-item-money-BWWQhRul.cjs';
@@ -9,7 +9,7 @@ export { Type as NestType } from '@nestjs/common';
9
9
  import { Reflector, BaseExceptionFilter } from '@nestjs/core';
10
10
  import { c as EntitlementService } from '../aggregation-CtIjlWHT.js';
11
11
  import { P as ProviderSpec } from '../di-CcNeq9v-.js';
12
- import { S as SubscriptionContractService } from '../subscription-contract.service-hD87MKyg.js';
12
+ import { S as SubscriptionContractService } from '../subscription-contract.service-wMqIPFkS.js';
13
13
  import '../promo.service-CBMm9RU7.js';
14
14
  import '../admin-audit.service-4P3Djdxk.js';
15
15
  import '../contract-line-item-money-BWWQhRul.js';
@@ -20,7 +20,7 @@ import {
20
20
  computeCarriedTrialEndsAt,
21
21
  computeNextPeriod,
22
22
  decideRenewal
23
- } from "../chunk-KOZYQS53.js";
23
+ } from "../chunk-2TACP6MQ.js";
24
24
  import {
25
25
  AddSubscriptionBundleDto,
26
26
  BILLING_FEATURE_UI_REGISTRY_TOKEN,
@@ -80,8 +80,7 @@ import {
80
80
  resolveBundleCancelEffectiveAt,
81
81
  resolvePlanAnchorDay,
82
82
  retreatOneCycle
83
- } from "../chunk-O4M5HC2B.js";
84
- import "../chunk-E2GFA4J2.js";
83
+ } from "../chunk-62WEP34A.js";
85
84
  import {
86
85
  AUDIT_CONTEXT_RESOLVER_TOKEN,
87
86
  CANCELLATION_NOTICE_DAYS_TOKEN,
@@ -98,20 +97,22 @@ import {
98
97
  classifyPlanDiff
99
98
  } from "../chunk-O6I7X4OG.js";
100
99
  import "../chunk-DQEU4UNU.js";
101
- import "../chunk-O2J2HDXA.js";
102
- import "../chunk-FAH62CAJ.js";
103
- import "../chunk-DR2SQNMX.js";
100
+ import "../chunk-LEZ6KYQ7.js";
104
101
  import {
105
102
  cyclesSoldFor,
106
103
  resolveBundlePriceNet
107
104
  } from "../chunk-KPEMTBOP.js";
108
- import "../chunk-YSC46BGM.js";
109
105
  import {
110
106
  SUBSCRIPTION_BUNDLE_CONFIG_TOKEN,
111
107
  SUBSCRIPTION_BUNDLE_REPOSITORY_TOKEN
112
- } from "../chunk-HRUVUY5E.js";
113
- import "../chunk-VBRYPU7V.js";
108
+ } from "../chunk-JMV6J7YP.js";
114
109
  import "../chunk-OUDSQ3PU.js";
110
+ import "../chunk-VBRYPU7V.js";
111
+ import "../chunk-AOQJEYOL.js";
112
+ import "../chunk-QUZU2M33.js";
113
+ import "../chunk-O2J2HDXA.js";
114
+ import "../chunk-FAH62CAJ.js";
115
+ import "../chunk-YSC46BGM.js";
115
116
  import "../chunk-57V6ZTI6.js";
116
117
  import {
117
118
  PLAN_CATALOG_READ_SINK_TOKEN,
@@ -132,9 +133,7 @@ import {
132
133
  } from "../chunk-UUDUEQXA.js";
133
134
  import "../chunk-5BCYJ3QY.js";
134
135
  import "../chunk-QSBMFY4I.js";
135
- import "../chunk-AOQJEYOL.js";
136
136
  import "../chunk-SABTXESR.js";
137
- import "../chunk-QUZU2M33.js";
138
137
  import "../chunk-SHUYVCID.js";
139
138
  export {
140
139
  AUDIT_CONTEXT_RESOLVER_TOKEN,
@@ -6,7 +6,7 @@ import {
6
6
  runSeedGateFromFile,
7
7
  seedGateExitCode,
8
8
  validateSeedAgainstSnapshot
9
- } from "../chunk-FKAWM74T.js";
9
+ } from "../chunk-45B4OQOO.js";
10
10
  import "../chunk-74SCJ4HF.js";
11
11
  import {
12
12
  ADVISORY_STRICT_MODE_CODES,
@@ -57,15 +57,12 @@ import {
57
57
  resolveValidityWindow,
58
58
  validateBundleDraft,
59
59
  validatePlanDraft
60
- } from "../chunk-AFZQ7GTQ.js";
60
+ } from "../chunk-GYI3E2YY.js";
61
61
  import "../chunk-O6I7X4OG.js";
62
62
  import "../chunk-DQEU4UNU.js";
63
- import "../chunk-O2J2HDXA.js";
64
63
  import "../chunk-KPEMTBOP.js";
65
- import "../chunk-YSC46BGM.js";
66
64
  import "../chunk-JVAEKTJ4.js";
67
65
  import "../chunk-OUDSQ3PU.js";
68
- import "../chunk-57V6ZTI6.js";
69
66
  import {
70
67
  BUNDLE_REPOSITORY_TOKEN,
71
68
  CATALOG_ENTRY_REPOSITORY_TOKEN,
@@ -76,8 +73,11 @@ import {
76
73
  PLAN_REPOSITORY_TOKEN,
77
74
  PROMOTION_REPOSITORY_TOKEN
78
75
  } from "../chunk-AOQJEYOL.js";
79
- import "../chunk-SABTXESR.js";
80
76
  import "../chunk-QUZU2M33.js";
77
+ import "../chunk-O2J2HDXA.js";
78
+ import "../chunk-YSC46BGM.js";
79
+ import "../chunk-57V6ZTI6.js";
80
+ import "../chunk-SABTXESR.js";
81
81
  import "../chunk-SHUYVCID.js";
82
82
  export {
83
83
  ADVISORY_STRICT_MODE_CODES,
@@ -1,7 +1,8 @@
1
- export { C as CheckoutOfferPricing, a as CheckoutOfferPricingInput, b as CheckoutOfferService, P as PricedCheckoutOffer } from '../checkout-offer.service-C5-YWUrp.cjs';
1
+ export { C as CheckoutOfferPricing, a as CheckoutOfferPricingInput, b as CheckoutOfferService, c as ConcludeCheckoutOfferWithin, d as ConcludedCheckoutOffer, P as PricedCheckoutOffer } from '../checkout-offer.service-Dknxgj6W.cjs';
2
2
  import { Type, CanActivate } from '@nestjs/common';
3
- export { a as CheckoutOfferControllerConfig, b as CheckoutOfferModule, C as CheckoutOfferModuleOptions } from '../checkout-offer.module-B1O0H8EQ.cjs';
3
+ export { a as CheckoutOfferConclusionOptions, b as CheckoutOfferControllerConfig, c as CheckoutOfferModule, C as CheckoutOfferModuleOptions } from '../checkout-offer.module-BOX9yAWq.cjs';
4
4
  import { CheckoutOfferSelection, CheckoutOfferSelectionUpdate } from '@saasicat/core';
5
+ import '../subscription-contract.service-wMqIPFkS.cjs';
5
6
  import '../promo.service-CBMm9RU7.cjs';
6
7
  import '../di-CcNeq9v-.cjs';
7
8
 
@@ -9,6 +10,11 @@ declare function buildCheckoutOfferController(guards: Array<Type<CanActivate>>):
9
10
 
10
11
  /** Repository for `checkout_offers`. */
11
12
  declare const CHECKOUT_OFFER_REPOSITORY_TOKEN: unique symbol;
13
+ /**
14
+ * The transaction an offer is concluded in: consuming it, writing its contract
15
+ * and the application's own writes commit or roll back together.
16
+ */
17
+ declare const CHECKOUT_OFFER_TRANSACTION_RUNNER_TOKEN: unique symbol;
12
18
 
13
19
  declare class CreateCheckoutOfferDto implements CheckoutOfferSelection {
14
20
  planKey: string;
@@ -26,4 +32,4 @@ declare class UpdateCheckoutOfferDto implements CheckoutOfferSelectionUpdate {
26
32
  validUntil?: string | null;
27
33
  }
28
34
 
29
- export { CHECKOUT_OFFER_REPOSITORY_TOKEN, CreateCheckoutOfferDto, UpdateCheckoutOfferDto, buildCheckoutOfferController };
35
+ export { CHECKOUT_OFFER_REPOSITORY_TOKEN, CHECKOUT_OFFER_TRANSACTION_RUNNER_TOKEN, CreateCheckoutOfferDto, UpdateCheckoutOfferDto, buildCheckoutOfferController };
@@ -1,7 +1,8 @@
1
- export { C as CheckoutOfferPricing, a as CheckoutOfferPricingInput, b as CheckoutOfferService, P as PricedCheckoutOffer } from '../checkout-offer.service-BIGkcWNe.js';
1
+ export { C as CheckoutOfferPricing, a as CheckoutOfferPricingInput, b as CheckoutOfferService, c as ConcludeCheckoutOfferWithin, d as ConcludedCheckoutOffer, P as PricedCheckoutOffer } from '../checkout-offer.service-BZumQOmZ.js';
2
2
  import { Type, CanActivate } from '@nestjs/common';
3
- export { a as CheckoutOfferControllerConfig, b as CheckoutOfferModule, C as CheckoutOfferModuleOptions } from '../checkout-offer.module-BVXEDXex.js';
3
+ export { a as CheckoutOfferConclusionOptions, b as CheckoutOfferControllerConfig, c as CheckoutOfferModule, C as CheckoutOfferModuleOptions } from '../checkout-offer.module-B-DF2I_-.js';
4
4
  import { CheckoutOfferSelection, CheckoutOfferSelectionUpdate } from '@saasicat/core';
5
+ import '../subscription-contract.service-wMqIPFkS.js';
5
6
  import '../promo.service-CBMm9RU7.js';
6
7
  import '../di-CcNeq9v-.js';
7
8
 
@@ -9,6 +10,11 @@ declare function buildCheckoutOfferController(guards: Array<Type<CanActivate>>):
9
10
 
10
11
  /** Repository for `checkout_offers`. */
11
12
  declare const CHECKOUT_OFFER_REPOSITORY_TOKEN: unique symbol;
13
+ /**
14
+ * The transaction an offer is concluded in: consuming it, writing its contract
15
+ * and the application's own writes commit or roll back together.
16
+ */
17
+ declare const CHECKOUT_OFFER_TRANSACTION_RUNNER_TOKEN: unique symbol;
12
18
 
13
19
  declare class CreateCheckoutOfferDto implements CheckoutOfferSelection {
14
20
  planKey: string;
@@ -26,4 +32,4 @@ declare class UpdateCheckoutOfferDto implements CheckoutOfferSelectionUpdate {
26
32
  validUntil?: string | null;
27
33
  }
28
34
 
29
- export { CHECKOUT_OFFER_REPOSITORY_TOKEN, CreateCheckoutOfferDto, UpdateCheckoutOfferDto, buildCheckoutOfferController };
35
+ export { CHECKOUT_OFFER_REPOSITORY_TOKEN, CHECKOUT_OFFER_TRANSACTION_RUNNER_TOKEN, CreateCheckoutOfferDto, UpdateCheckoutOfferDto, buildCheckoutOfferController };
@@ -1,25 +1,28 @@
1
1
  import "../chunk-DFOW3JVO.js";
2
2
  import {
3
3
  CHECKOUT_OFFER_REPOSITORY_TOKEN,
4
+ CHECKOUT_OFFER_TRANSACTION_RUNNER_TOKEN,
4
5
  CheckoutOfferModule,
5
6
  CheckoutOfferPricing,
6
7
  CheckoutOfferService,
7
8
  CreateCheckoutOfferDto,
8
9
  UpdateCheckoutOfferDto,
9
10
  buildCheckoutOfferController
10
- } from "../chunk-L7PTXFJE.js";
11
- import "../chunk-FAH62CAJ.js";
12
- import "../chunk-DR2SQNMX.js";
11
+ } from "../chunk-H3GVIQVK.js";
12
+ import "../chunk-LEZ6KYQ7.js";
13
13
  import "../chunk-KPEMTBOP.js";
14
+ import "../chunk-VBRYPU7V.js";
15
+ import "../chunk-AOQJEYOL.js";
16
+ import "../chunk-FAH62CAJ.js";
14
17
  import "../chunk-YSC46BGM.js";
15
18
  import "../chunk-UUDUEQXA.js";
16
19
  import "../chunk-5BCYJ3QY.js";
17
20
  import "../chunk-QSBMFY4I.js";
18
- import "../chunk-AOQJEYOL.js";
19
21
  import "../chunk-SABTXESR.js";
20
22
  import "../chunk-SHUYVCID.js";
21
23
  export {
22
24
  CHECKOUT_OFFER_REPOSITORY_TOKEN,
25
+ CHECKOUT_OFFER_TRANSACTION_RUNNER_TOKEN,
23
26
  CheckoutOfferModule,
24
27
  CheckoutOfferPricing,
25
28
  CheckoutOfferService,
@@ -1,11 +1,18 @@
1
1
  import { Type, CanActivate, DynamicModule, ForwardReference, Provider } from '@nestjs/common';
2
- import { CheckoutOfferRepository, BundleRepository, PlanRepository, PromotionRepository, CatalogEntryRepository } from '@saasicat/core';
2
+ import { SubscriptionContractRepository, TransactionRunner, CheckoutOfferRepository, BundleRepository, PlanRepository, PromotionRepository, CatalogEntryRepository } from '@saasicat/core';
3
3
  import { P as ProviderSpec } from './di-CcNeq9v-.js';
4
4
 
5
5
  interface CheckoutOfferControllerConfig {
6
6
  /** Class-level guards; `[]` for auth-free public endpoints. */
7
7
  guards: Array<Type<CanActivate>>;
8
8
  }
9
+ /** What `CheckoutOfferService.conclude` writes through, and in one transaction. */
10
+ interface CheckoutOfferConclusionOptions {
11
+ /** Writes the contract an offer becomes, on the conclusion's transaction. */
12
+ subscriptionContractRepository: ProviderSpec<SubscriptionContractRepository>;
13
+ /** The transaction consuming the offer, the contract and the application's writes share. */
14
+ transactionRunner: ProviderSpec<TransactionRunner>;
15
+ }
9
16
  interface CheckoutOfferModuleOptions {
10
17
  checkoutOfferRepository: ProviderSpec<CheckoutOfferRepository>;
11
18
  /**
@@ -30,6 +37,12 @@ interface CheckoutOfferModuleOptions {
30
37
  * skipped (graceful).
31
38
  */
32
39
  catalogEntryRepository?: ProviderSpec<CatalogEntryRepository>;
40
+ /**
41
+ * Enables `CheckoutOfferService.conclude`, which consumes an offer and
42
+ * writes its contract in one transaction. Without it, `conclude` refuses
43
+ * to run rather than writing the two apart.
44
+ */
45
+ conclusion?: CheckoutOfferConclusionOptions;
33
46
  /** Controller mount for `/public/checkout-offer`. Omitted = service only. */
34
47
  controller?: CheckoutOfferControllerConfig;
35
48
  imports?: Array<Type<unknown> | DynamicModule | Promise<DynamicModule> | ForwardReference>;
@@ -40,4 +53,4 @@ declare class CheckoutOfferModule {
40
53
  static forRoot(options: CheckoutOfferModuleOptions): DynamicModule;
41
54
  }
42
55
 
43
- export { type CheckoutOfferModuleOptions as C, type CheckoutOfferControllerConfig as a, CheckoutOfferModule as b };
56
+ export { type CheckoutOfferModuleOptions as C, type CheckoutOfferConclusionOptions as a, type CheckoutOfferControllerConfig as b, CheckoutOfferModule as c };
@@ -1,11 +1,18 @@
1
1
  import { Type, CanActivate, DynamicModule, ForwardReference, Provider } from '@nestjs/common';
2
- import { CheckoutOfferRepository, BundleRepository, PlanRepository, PromotionRepository, CatalogEntryRepository } from '@saasicat/core';
2
+ import { SubscriptionContractRepository, TransactionRunner, CheckoutOfferRepository, BundleRepository, PlanRepository, PromotionRepository, CatalogEntryRepository } from '@saasicat/core';
3
3
  import { P as ProviderSpec } from './di-CcNeq9v-.cjs';
4
4
 
5
5
  interface CheckoutOfferControllerConfig {
6
6
  /** Class-level guards; `[]` for auth-free public endpoints. */
7
7
  guards: Array<Type<CanActivate>>;
8
8
  }
9
+ /** What `CheckoutOfferService.conclude` writes through, and in one transaction. */
10
+ interface CheckoutOfferConclusionOptions {
11
+ /** Writes the contract an offer becomes, on the conclusion's transaction. */
12
+ subscriptionContractRepository: ProviderSpec<SubscriptionContractRepository>;
13
+ /** The transaction consuming the offer, the contract and the application's writes share. */
14
+ transactionRunner: ProviderSpec<TransactionRunner>;
15
+ }
9
16
  interface CheckoutOfferModuleOptions {
10
17
  checkoutOfferRepository: ProviderSpec<CheckoutOfferRepository>;
11
18
  /**
@@ -30,6 +37,12 @@ interface CheckoutOfferModuleOptions {
30
37
  * skipped (graceful).
31
38
  */
32
39
  catalogEntryRepository?: ProviderSpec<CatalogEntryRepository>;
40
+ /**
41
+ * Enables `CheckoutOfferService.conclude`, which consumes an offer and
42
+ * writes its contract in one transaction. Without it, `conclude` refuses
43
+ * to run rather than writing the two apart.
44
+ */
45
+ conclusion?: CheckoutOfferConclusionOptions;
33
46
  /** Controller mount for `/public/checkout-offer`. Omitted = service only. */
34
47
  controller?: CheckoutOfferControllerConfig;
35
48
  imports?: Array<Type<unknown> | DynamicModule | Promise<DynamicModule> | ForwardReference>;
@@ -40,4 +53,4 @@ declare class CheckoutOfferModule {
40
53
  static forRoot(options: CheckoutOfferModuleOptions): DynamicModule;
41
54
  }
42
55
 
43
- export { type CheckoutOfferModuleOptions as C, type CheckoutOfferControllerConfig as a, CheckoutOfferModule as b };
56
+ export { type CheckoutOfferModuleOptions as C, type CheckoutOfferConclusionOptions as a, type CheckoutOfferControllerConfig as b, CheckoutOfferModule as c };
@@ -1,4 +1,5 @@
1
- import { PlanCatalog, PlanRepository, BundleRepository, PromotionRepository, CheckoutOfferPriceBreakdown, CheckoutOfferLineItem, CheckoutOfferPromotionSnapshot, CheckoutOfferPromoCodeSnapshot, CheckoutOfferRow, CheckoutOfferRepository, CatalogEntryRepository, CheckoutOfferFilter, CheckoutOfferSelection, CheckoutOfferSelectionUpdate } from '@saasicat/core';
1
+ import { PlanCatalog, PlanRepository, BundleRepository, PromotionRepository, CheckoutOfferPriceBreakdown, CheckoutOfferLineItem, CheckoutOfferPromotionSnapshot, CheckoutOfferPromoCodeSnapshot, CheckoutOfferRow, CheckoutOfferRepository, CatalogEntryRepository, TransactionRunner, CheckoutOfferFilter, CheckoutOfferSelection, CheckoutOfferSelectionUpdate, TransactionContext, SubscriptionContractRecord } from '@saasicat/core';
2
+ import { S as SubscriptionContractService, C as CreateContractFromOfferOptions } from './subscription-contract.service-wMqIPFkS.js';
2
3
  import { a as PromoCodesService } from './promo.service-CBMm9RU7.js';
3
4
 
4
5
  type Cycle = 'monthly' | 'yearly';
@@ -56,13 +57,26 @@ declare class CheckoutOfferPricing {
56
57
  private pricePromoCode;
57
58
  }
58
59
 
60
+ /** What concluding an offer produced: the consumed offer and its contract. */
61
+ interface ConcludedCheckoutOffer {
62
+ offer: CheckoutOfferRow;
63
+ contract: SubscriptionContractRecord;
64
+ }
65
+ /**
66
+ * The application's own writes that belong to concluding an offer, such as
67
+ * starting its subscription. They run on the same transaction, after the offer
68
+ * is consumed and its contract written; throwing undoes all of it.
69
+ */
70
+ type ConcludeCheckoutOfferWithin = (tx: TransactionContext, concluded: ConcludedCheckoutOffer) => Promise<void>;
59
71
  declare class CheckoutOfferService {
60
72
  private readonly repo;
61
73
  private readonly pricing;
62
74
  private readonly bundles;
63
75
  private readonly plans;
64
76
  private readonly catalogEntries;
65
- constructor(repo: CheckoutOfferRepository, pricing: CheckoutOfferPricing, bundles?: BundleRepository | null, plans?: PlanRepository | null, catalogEntries?: CatalogEntryRepository | null);
77
+ private readonly contracts;
78
+ private readonly transactions;
79
+ constructor(repo: CheckoutOfferRepository, pricing: CheckoutOfferPricing, bundles?: BundleRepository | null, plans?: PlanRepository | null, catalogEntries?: CatalogEntryRepository | null, contracts?: SubscriptionContractService | null, transactions?: TransactionRunner | null);
66
80
  list(filter: CheckoutOfferFilter): Promise<CheckoutOfferRow[]>;
67
81
  getById(id: string): Promise<CheckoutOfferRow>;
68
82
  /** Prices what the caller chose and stores it as an open offer. */
@@ -89,6 +103,43 @@ declare class CheckoutOfferService {
89
103
  * is kept for amounts that no longer match.
90
104
  */
91
105
  consume(id: string): Promise<CheckoutOfferRow>;
106
+ /**
107
+ * Consumes an offer and creates its contract in one transaction, together
108
+ * with the application's own writes in `within`, so an offer is never
109
+ * consumed without its contract, and no contract is written for an offer
110
+ * that stays open.
111
+ *
112
+ * Everything that can refuse runs before the transaction: what `consume`
113
+ * checks, and the checks the contract the offer becomes is held to. Inside
114
+ * it, the offer is consumed on the condition that it is still open, the
115
+ * contract is written, and `within` runs; an error in any of them undoes
116
+ * the others, and the offer can be concluded again.
117
+ *
118
+ * An offer that is already concluded for the same tenant answers with its
119
+ * contract rather than a refusal, so a caller retrying after a lost answer,
120
+ * or losing a race to another call for that tenant, gets the conclusion that
121
+ * stands; `within` does not run again, its writes committed with it. A
122
+ * conclusion for another tenant is refused with
123
+ * `CHECKOUT_OFFER_ALREADY_CONSUMED`, and an offer changed between the checks
124
+ * and the transaction with `CHECKOUT_OFFER_CHANGED`, nothing written.
125
+ *
126
+ * A promo code on the offer is redeemed in `within`, with
127
+ * `PromoCodesService.redeemInTransaction` on the same transaction. Before
128
+ * the transaction the code is only checked the way pricing checks it; the
129
+ * redemption checks the rest, the customer and the remaining redemptions,
130
+ * where a refusal still undoes everything. Nothing checks the code again
131
+ * after `within`, so a redemption that takes its last slot does not refuse
132
+ * the offer it was redeemed for.
133
+ */
134
+ conclude(id: string, options: CreateContractFromOfferOptions, within?: ConcludeCheckoutOfferWithin): Promise<ConcludedCheckoutOffer>;
135
+ private assertConsumable;
136
+ /**
137
+ * The conclusion that stands for this tenant, if the offer has one. An offer
138
+ * carries no tenant and its id travels in a link, so a conclusion for
139
+ * another tenant is not an answer: that caller is refused as before.
140
+ */
141
+ private standingConclusion;
142
+ private conclusionPorts;
92
143
  /**
93
144
  * #35 P6 — server-side requires validation: the dependencies of all
94
145
  * features (plan ∪ selected bundles) must be covered within the
@@ -112,4 +163,4 @@ declare class CheckoutOfferService {
112
163
  private isExpired;
113
164
  }
114
165
 
115
- export { CheckoutOfferPricing as C, type PricedCheckoutOffer as P, type CheckoutOfferPricingInput as a, CheckoutOfferService as b };
166
+ export { CheckoutOfferPricing as C, type PricedCheckoutOffer as P, type CheckoutOfferPricingInput as a, CheckoutOfferService as b, type ConcludeCheckoutOfferWithin as c, type ConcludedCheckoutOffer as d };