@saasicat/nest 1.0.0-rc.2 → 1.0.0-rc.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/README.md +44 -8
  2. package/dist/.build-stamp +1 -1
  3. package/dist/_entries.cjs +10214 -4796
  4. package/dist/admin/index.d.cts +5 -4
  5. package/dist/admin/index.d.ts +5 -4
  6. package/dist/admin/index.js +13 -9
  7. package/dist/{admin-resources.module-CpvzcfCc.d.cts → admin-resources.module-B4XNvdH-.d.cts} +1 -2
  8. package/dist/{admin-resources.module-DF5_Q7RD.d.ts → admin-resources.module-CWqdI9BV.d.ts} +1 -2
  9. package/dist/{admin-stats.service-TYiSUwIR.d.ts → admin-stats.service-CsLNt19Z.d.ts} +11 -7
  10. package/dist/{admin-stats.service-DfKupywR.d.cts → admin-stats.service-DrZrU8IJ.d.cts} +11 -7
  11. package/dist/{aggregation-B3CD0v_d.d.cts → aggregation-ChpZDh0g.d.ts} +39 -6
  12. package/dist/{aggregation-BFzW07DE.d.ts → aggregation-OpnhwzF5.d.cts} +39 -6
  13. package/dist/billing/index.d.cts +448 -108
  14. package/dist/billing/index.d.ts +448 -108
  15. package/dist/billing/index.js +108 -41
  16. package/dist/catalog/index.d.cts +74 -45
  17. package/dist/catalog/index.d.ts +74 -45
  18. package/dist/catalog/index.js +21 -13
  19. package/dist/catalog.module-C9O7BxRq.d.ts +180 -0
  20. package/dist/catalog.module-T-I5Ricz.d.cts +180 -0
  21. package/dist/checkout-offer/index.d.cts +17 -22
  22. package/dist/checkout-offer/index.d.ts +17 -22
  23. package/dist/checkout-offer/index.js +21 -7
  24. package/dist/checkout-offer.module-DFhGy6JO.d.cts +58 -0
  25. package/dist/checkout-offer.module-x7SP_dX4.d.ts +58 -0
  26. package/dist/checkout-offer.service-BeHgYikI.d.ts +242 -0
  27. package/dist/checkout-offer.service-CCVxG9b2.d.cts +242 -0
  28. package/dist/chunk-34XVPR6P.js +222 -0
  29. package/dist/{chunk-AOQJEYOL.js → chunk-3QVGA6JX.js} +3 -1
  30. package/dist/chunk-3RLWML2S.js +896 -0
  31. package/dist/{chunk-TXC3LHHB.js → chunk-4SDSYV4A.js} +21 -13
  32. package/dist/chunk-55QEDAMX.js +946 -0
  33. package/dist/chunk-5CKC7O52.js +140 -0
  34. package/dist/chunk-6PJE7EWO.js +185 -0
  35. package/dist/chunk-7NJKVC5X.js +353 -0
  36. package/dist/{chunk-RUNZ3X4C.js → chunk-AOS2JPDD.js} +1263 -185
  37. package/dist/{chunk-WOVQPXV4.js → chunk-CQ2ZZMTD.js} +702 -725
  38. package/dist/{chunk-NA6O63B7.js → chunk-F5X66HF5.js} +5 -5
  39. package/dist/chunk-FEQZCWUL.js +174 -0
  40. package/dist/{chunk-VIJ5NJWC.js → chunk-G6EZWECL.js} +107 -31
  41. package/dist/chunk-G7RQO2XO.js +0 -0
  42. package/dist/{chunk-N3L3AICU.js → chunk-GPQWGA6B.js} +5 -6
  43. package/dist/chunk-GZTL64QK.js +7 -0
  44. package/dist/{chunk-SABTXESR.js → chunk-J7NJRK3K.js} +17 -1
  45. package/dist/{chunk-SEPN52AD.js → chunk-JBCC6C3M.js} +566 -307
  46. package/dist/{chunk-I7GTA3RX.js → chunk-JQRA724W.js} +1 -1
  47. package/dist/{chunk-WXYJHZCN.js → chunk-JVAEKTJ4.js} +2 -14
  48. package/dist/chunk-KPEMTBOP.js +26 -0
  49. package/dist/chunk-LF6J4YYN.js +301 -0
  50. package/dist/{chunk-LTT736P3.js → chunk-LLYVYRGJ.js} +228 -387
  51. package/dist/{chunk-6Z7JR4EW.js → chunk-M47BNEY2.js} +1897 -633
  52. package/dist/{chunk-KFT5AIIH.js → chunk-NDLC5GYK.js} +206 -24
  53. package/dist/{chunk-O2J2HDXA.js → chunk-OOTEXP47.js} +3 -8
  54. package/dist/chunk-QPVDCKYS.js +702 -0
  55. package/dist/{chunk-NHVDCYK5.js → chunk-R5YCJBHY.js} +71 -106
  56. package/dist/{chunk-XBYAFEOR.js → chunk-S33SO5XX.js} +1 -1
  57. package/dist/chunk-SZ7RFPXA.js +10 -0
  58. package/dist/chunk-TBBZWZQT.js +98 -0
  59. package/dist/chunk-TOEFN7DN.js +295 -0
  60. package/dist/{chunk-7ZEGFL42.js → chunk-WHJSYKNC.js} +6 -6
  61. package/dist/chunk-WOEJ6K7M.js +55 -0
  62. package/dist/{chunk-AU3OOREM.js → chunk-XSSWYPP5.js} +16 -3
  63. package/dist/contract-line-item-money-B0Z3Ogur.d.cts +43 -0
  64. package/dist/contract-line-item-money-B0Z3Ogur.d.ts +43 -0
  65. package/dist/discovery/index.d.cts +2 -2
  66. package/dist/discovery/index.d.ts +2 -2
  67. package/dist/discovery/index.js +6 -6
  68. package/dist/{discovery.scanner-9cMqF95o.d.cts → discovery.scanner-DXKc6JkV.d.cts} +1 -1
  69. package/dist/{discovery.scanner-9cMqF95o.d.ts → discovery.scanner-DXKc6JkV.d.ts} +1 -1
  70. package/dist/{enforce-quota.interceptor-Df0e3OWe.d.cts → enforce-quota.interceptor-BFyVdWe8.d.cts} +6 -6
  71. package/dist/{enforce-quota.interceptor-DW3etVyI.d.ts → enforce-quota.interceptor-GaBXboGx.d.ts} +6 -6
  72. package/dist/entitlement/index.d.cts +27 -4
  73. package/dist/entitlement/index.d.ts +27 -4
  74. package/dist/entitlement/index.js +13 -8
  75. package/dist/index.d.cts +209 -28
  76. package/dist/index.d.ts +209 -28
  77. package/dist/index.js +285 -163
  78. package/dist/issuer-identity.check-CBO0vcsq.d.cts +197 -0
  79. package/dist/issuer-identity.check-Cz9CEMkZ.d.ts +197 -0
  80. package/dist/{module-options-COSEb0VW.d.ts → module-options-DmQ3G4sZ.d.ts} +135 -41
  81. package/dist/{module-options-BG3MzQva.d.cts → module-options-qAOwinHR.d.cts} +135 -41
  82. package/dist/payment-callback.service-CR84Xw1Z.d.cts +125 -0
  83. package/dist/payment-callback.service-CR84Xw1Z.d.ts +125 -0
  84. package/dist/payments/index.cjs +4 -0
  85. package/dist/payments/index.d.cts +187 -0
  86. package/dist/payments/index.d.ts +187 -0
  87. package/dist/payments/index.js +141 -0
  88. package/dist/payments.module-BdUq9ot_.d.cts +57 -0
  89. package/dist/payments.module-CQGv_VG7.d.ts +57 -0
  90. package/dist/plan-catalog-source-DWe-BGY1.d.cts +21 -0
  91. package/dist/plan-catalog-source-DWe-BGY1.d.ts +21 -0
  92. package/dist/{plan-resolution-Cgo_TR1H.d.cts → plan-resolution-CwUy_OqC.d.cts} +14 -0
  93. package/dist/{plan-resolution-Cgo_TR1H.d.ts → plan-resolution-CwUy_OqC.d.ts} +14 -0
  94. package/dist/{plan-versions.service-DChhbK6h.d.ts → plan-versions.service-C0N-Em2W.d.ts} +38 -64
  95. package/dist/{plan-versions.service-TPEtbN0r.d.cts → plan-versions.service-DARLh-qI.d.cts} +38 -64
  96. package/dist/platform/index.d.cts +52 -23
  97. package/dist/platform/index.d.ts +52 -23
  98. package/dist/platform/index.js +57 -36
  99. package/dist/promo/index.d.cts +23 -8
  100. package/dist/promo/index.d.ts +23 -8
  101. package/dist/promo/index.js +15 -7
  102. package/dist/{promo.module-DtvIycpt.d.cts → promo.module-CKkECmU8.d.cts} +9 -3
  103. package/dist/{promo.module-Z8h1PNX-.d.ts → promo.module-DuIJdK3U.d.ts} +9 -3
  104. package/dist/promo.service-BYBu6dy3.d.cts +215 -0
  105. package/dist/promo.service-CFjfF0Xm.d.ts +215 -0
  106. package/dist/registration/index.d.cts +104 -47
  107. package/dist/registration/index.d.ts +104 -47
  108. package/dist/registration/index.js +15 -9
  109. package/dist/subscriber/index.cjs +4 -0
  110. package/dist/subscriber/index.d.cts +28 -0
  111. package/dist/subscriber/index.d.ts +28 -0
  112. package/dist/subscriber/index.js +16 -0
  113. package/dist/subscriber.service-5W6Gi-1B.d.cts +68 -0
  114. package/dist/subscriber.service-5W6Gi-1B.d.ts +68 -0
  115. package/dist/subscription-contract/index.d.cts +4 -2
  116. package/dist/subscription-contract/index.d.ts +4 -2
  117. package/dist/subscription-contract/index.js +13 -5
  118. package/dist/{subscription-contract.module-fDFdQ0jm.d.ts → subscription-contract.module-BfIRpc1o.d.ts} +8 -2
  119. package/dist/{subscription-contract.module-CEK8HS5-.d.cts → subscription-contract.module-DTMuUJYd.d.cts} +8 -2
  120. package/dist/{subscription-contract.service-hD87MKyg.d.cts → subscription-contract.service-CPZrTM9C.d.ts} +32 -3
  121. package/dist/{subscription-contract.service-hD87MKyg.d.ts → subscription-contract.service-CfNApLuP.d.cts} +32 -3
  122. package/dist/tenant-billing.controller-BLqbsONe.d.ts +812 -0
  123. package/dist/tenant-billing.controller-DKbAqTbM.d.cts +812 -0
  124. package/dist/tenant-billing.module-ClbjzocC.d.cts +364 -0
  125. package/dist/tenant-billing.module-DohgJJ2m.d.ts +364 -0
  126. package/dist/tenant-billing.tokens-G-1xOlMr.d.cts +129 -0
  127. package/dist/tenant-billing.tokens-G-1xOlMr.d.ts +129 -0
  128. package/dist/testing/index.d.cts +44 -17
  129. package/dist/testing/index.d.ts +44 -17
  130. package/dist/testing/index.js +189 -40
  131. package/package.json +24 -4
  132. package/dist/catalog.module-BDDO6iwq.d.cts +0 -103
  133. package/dist/catalog.module-BTnMsE6u.d.ts +0 -103
  134. package/dist/checkout-offer.module-CMtiQJTT.d.ts +0 -39
  135. package/dist/checkout-offer.module-CVpWrzbe.d.cts +0 -39
  136. package/dist/checkout-offer.service-BLOv2HOo.d.cts +0 -49
  137. package/dist/checkout-offer.service-BLOv2HOo.d.ts +0 -49
  138. package/dist/chunk-57V6ZTI6.js +0 -22
  139. package/dist/chunk-BSK6YBLI.js +0 -87
  140. package/dist/chunk-DQKCK7DX.js +0 -217
  141. package/dist/chunk-VXEYLNIB.js +0 -632
  142. package/dist/chunk-WUDYIPYH.js +0 -715
  143. package/dist/define-saasicat-DROBe-b1.d.ts +0 -69
  144. package/dist/define-saasicat-vGBCkFys.d.cts +0 -69
  145. package/dist/promo.service-BJbKAmw3.d.cts +0 -112
  146. package/dist/promo.service-BJbKAmw3.d.ts +0 -112
  147. package/dist/tenant-billing.controller-BQ67mlmI.d.ts +0 -438
  148. package/dist/tenant-billing.controller-DS8kB8a7.d.cts +0 -438
  149. package/dist/tenant-billing.module-BznC3XtW.d.cts +0 -299
  150. package/dist/tenant-billing.module-wUbsQSFG.d.ts +0 -299
  151. /package/dist/{chunk-2FR6ZL7R.js → chunk-BHZOH2DY.js} +0 -0
  152. /package/dist/{chunk-2SLTRKXC.js → chunk-DFOW3JVO.js} +0 -0
  153. /package/dist/{chunk-DBZGV3HC.js → chunk-DU56UKEH.js} +0 -0
  154. /package/dist/{chunk-OYNHOY45.js → chunk-GAUEVIAE.js} +0 -0
@@ -1,15 +1,127 @@
1
- import {
2
- appendImplicitDiscountLineItem
3
- } from "./chunk-BSK6YBLI.js";
4
1
  import {
5
2
  SUBSCRIPTION_CONTRACT_REPOSITORY_TOKEN
6
3
  } from "./chunk-VBRYPU7V.js";
4
+ import {
5
+ assertContractWindow,
6
+ assertLinesAddUp,
7
+ assertNoNegativeDiscount,
8
+ assertOnePlanLine,
9
+ assertTaxRatePercent,
10
+ grossSharesOf,
11
+ recordContractLinesMoney,
12
+ round2
13
+ } from "./chunk-6PJE7EWO.js";
14
+ import {
15
+ SubscriberService
16
+ } from "./chunk-LF6J4YYN.js";
7
17
  import {
8
18
  __name
9
19
  } from "./chunk-SHUYVCID.js";
10
20
 
11
21
  // src/subscription-contract/subscription-contract.service.ts
12
22
  import { ConflictException, Inject, Injectable, NotFoundException, UnprocessableEntityException } from "@nestjs/common";
23
+
24
+ // src/checkout-offer/discount-line-items.ts
25
+ function appendImplicitDiscountLineItem(input) {
26
+ return withGrossShares(linesWithDiscount(input), input.priceBreakdown.vatRate);
27
+ }
28
+ __name(appendImplicitDiscountLineItem, "appendImplicitDiscountLineItem");
29
+ function linesWithDiscount(input) {
30
+ const lineItems = input.lineItems.map((item) => cloneCheckoutOfferLineItem(item));
31
+ const discountNet = roundMoney(input.priceBreakdown.regularNet - input.priceBreakdown.effectiveNet);
32
+ if (lineItems.some((item) => item.kind === "discount" && !isGeneratedDiscount(item))) {
33
+ return lineItems;
34
+ }
35
+ const lineItemsWithoutGeneratedDiscount = lineItems.filter((item) => !isGeneratedDiscount(item));
36
+ if (discountNet <= 0) {
37
+ return lineItemsWithoutGeneratedDiscount;
38
+ }
39
+ return [
40
+ ...lineItemsWithoutGeneratedDiscount,
41
+ createDiscountLineItem(input, discountNet)
42
+ ];
43
+ }
44
+ __name(linesWithDiscount, "linesWithDiscount");
45
+ function cloneCheckoutOfferLineItem(item) {
46
+ return {
47
+ ...item,
48
+ featuresSnapshot: [
49
+ ...item.featuresSnapshot ?? []
50
+ ],
51
+ quotaEffectsSnapshot: {
52
+ ...item.quotaEffectsSnapshot ?? {}
53
+ },
54
+ metadata: cloneRecord(item.metadata)
55
+ };
56
+ }
57
+ __name(cloneCheckoutOfferLineItem, "cloneCheckoutOfferLineItem");
58
+ function withGrossShares(lines, vatRate) {
59
+ const shares = grossSharesOf(lines, vatRate);
60
+ return lines.map((line, index) => {
61
+ const priceGross = shares[index];
62
+ if (!isGeneratedDiscount(line)) return {
63
+ ...line,
64
+ priceGross
65
+ };
66
+ return {
67
+ ...line,
68
+ priceGross,
69
+ metadata: {
70
+ ...line.metadata,
71
+ discountGross: -priceGross
72
+ }
73
+ };
74
+ });
75
+ }
76
+ __name(withGrossShares, "withGrossShares");
77
+ function createDiscountLineItem(input, discountNet) {
78
+ const promoCode = input.promoCodeSnapshot ?? null;
79
+ const firstPromotion = input.promotionSnapshots?.[0] ?? null;
80
+ return {
81
+ kind: "discount",
82
+ sourceKey: promoCode?.code ?? firstPromotion?.id ?? "price-discount",
83
+ sourceVersionId: null,
84
+ titleSnapshot: promoCode?.label ?? firstPromotion?.label ?? "Discount",
85
+ descriptionSnapshot: promoCode ? `Promo-Code ${promoCode.code}` : null,
86
+ quantity: 1,
87
+ unit: null,
88
+ priceNet: -discountNet,
89
+ billingCycle: input.billingCycle,
90
+ featuresSnapshot: [],
91
+ quotaEffectsSnapshot: {},
92
+ metadata: {
93
+ generated: true,
94
+ source: promoCode ? "promo_code" : firstPromotion ? "promotion" : "price_breakdown",
95
+ discountNet,
96
+ promotionSnapshots: cloneArray(input.promotionSnapshots ?? []),
97
+ promoCodeSnapshot: cloneJsonValue(promoCode)
98
+ }
99
+ };
100
+ }
101
+ __name(createDiscountLineItem, "createDiscountLineItem");
102
+ function roundMoney(value) {
103
+ return Math.round((value + Number.EPSILON) * 100) / 100;
104
+ }
105
+ __name(roundMoney, "roundMoney");
106
+ function isGeneratedDiscount(item) {
107
+ return item.kind === "discount" && item.metadata?.generated === true;
108
+ }
109
+ __name(isGeneratedDiscount, "isGeneratedDiscount");
110
+ function cloneRecord(value) {
111
+ if (!value) return null;
112
+ return cloneJsonValue(value);
113
+ }
114
+ __name(cloneRecord, "cloneRecord");
115
+ function cloneArray(value) {
116
+ return cloneJsonValue(value);
117
+ }
118
+ __name(cloneArray, "cloneArray");
119
+ function cloneJsonValue(value) {
120
+ return value == null ? value : JSON.parse(JSON.stringify(value));
121
+ }
122
+ __name(cloneJsonValue, "cloneJsonValue");
123
+
124
+ // src/subscription-contract/subscription-contract.service.ts
13
125
  import { CONTRACT_ERROR_CODES } from "@saasicat/core";
14
126
  function _ts_decorate(decorators, target, key, desc) {
15
127
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -33,8 +145,10 @@ var SubscriptionContractService = class {
33
145
  __name(this, "SubscriptionContractService");
34
146
  }
35
147
  repo;
36
- constructor(repo) {
148
+ subscribers;
149
+ constructor(repo, subscribers) {
37
150
  this.repo = repo;
151
+ this.subscribers = subscribers;
38
152
  }
39
153
  list(filter) {
40
154
  return this.repo.list(filter);
@@ -69,9 +183,36 @@ var SubscriptionContractService = class {
69
183
  }
70
184
  return subscriptionContractToInvoiceSnapshot(contract);
71
185
  }
72
- async create(data) {
186
+ /**
187
+ * Writes the contract between the tenant's subscriber and the issuer
188
+ * `config/saas.yaml` names, copying both as they stand. With `tx`, the
189
+ * contract is written on that transaction and undone with it, and the
190
+ * subscriber is read there too — so one created earlier on the same
191
+ * transaction is found.
192
+ *
193
+ * Refused with `SUBSCRIBER_REQUIRED` for a tenant without a subscriber:
194
+ * nothing is agreed without the party to it.
195
+ */
196
+ async create(data, tx) {
73
197
  this.assertCreateData(data);
74
- return this.repo.create(this.cloneCreateData(data));
198
+ const parties = await this.subscribers.contractPartiesFor(data.tenantId, tx);
199
+ return this.repo.create({
200
+ ...this.cloneCreateData(data),
201
+ parties
202
+ }, tx);
203
+ }
204
+ /**
205
+ * Refuses, with `SUBSCRIBER_REQUIRED`, a contract this tenant could not
206
+ * have. For callers that change something before the contract is written —
207
+ * closing the one in force, changing a plan — and must refuse before that
208
+ * rather than after.
209
+ */
210
+ async assertPartyFor(tenantId, tx) {
211
+ await this.subscribers.requireForTenant(tenantId, tx);
212
+ }
213
+ /** The contract concluded from a checkout offer, or `null` when none was. */
214
+ findByOriginalOfferId(offerId, tx) {
215
+ return this.repo.findByOriginalOfferId(offerId, tx);
75
216
  }
76
217
  async terminate(contractId, data) {
77
218
  const existing = await this.getById(contractId);
@@ -79,6 +220,13 @@ var SubscriptionContractService = class {
79
220
  return this.repo.terminate(contractId, data);
80
221
  }
81
222
  async replaceActiveContract(tenantId, data, terminateAt) {
223
+ const nextData = {
224
+ ...data,
225
+ tenantId,
226
+ effectiveFrom: data.effectiveFrom ?? terminateAt
227
+ };
228
+ this.assertCreateData(nextData);
229
+ await this.assertPartyFor(tenantId);
82
230
  const previous = await this.repo.findActiveByTenantId(tenantId, terminateAt);
83
231
  if (previous) {
84
232
  await this.terminate(previous.id, {
@@ -86,11 +234,7 @@ var SubscriptionContractService = class {
86
234
  status: "superseded"
87
235
  });
88
236
  }
89
- const next = await this.create({
90
- ...data,
91
- tenantId,
92
- effectiveFrom: data.effectiveFrom ?? terminateAt
93
- });
237
+ const next = await this.create(nextData);
94
238
  return {
95
239
  previous,
96
240
  next
@@ -107,9 +251,22 @@ var SubscriptionContractService = class {
107
251
  }
108
252
  });
109
253
  }
254
+ return this.dataFromOffer(offer, options);
255
+ }
256
+ /**
257
+ * The contract an offer becomes, checked the way `create` checks it, before
258
+ * anything is written. Unlike `createDataFromOffer` it takes an offer that
259
+ * is still open: concluding an offer refuses a contract it could not create
260
+ * before the offer is consumed, rather than after.
261
+ */
262
+ prepareFromOffer(offer, options) {
263
+ const data = this.dataFromOffer(offer, options);
264
+ this.assertCreateData(data);
265
+ return data;
266
+ }
267
+ dataFromOffer(offer, options) {
110
268
  const lineItems = this.lineItemsFromOffer(offer);
111
269
  return {
112
- projectKey: offer.projectKey,
113
270
  tenantId: options.tenantId,
114
271
  status: options.status ?? "active",
115
272
  effectiveFrom: options.effectiveFrom,
@@ -142,13 +299,17 @@ var SubscriptionContractService = class {
142
299
  message: "A checkout offer can yield only one contract, and only once its line items are frozen."
143
300
  });
144
301
  }
145
- return appendImplicitDiscountLineItem({
302
+ const lines = appendImplicitDiscountLineItem({
146
303
  billingCycle: offer.billingCycle,
147
304
  priceBreakdown: offer.priceBreakdown,
148
305
  lineItems: source,
149
306
  promotionSnapshots: offer.promotionSnapshots ?? [],
150
307
  promoCodeSnapshot: offer.promoCodeSnapshot ?? null
151
308
  }).map((item) => this.offerLineItemToContractLineItem(item));
309
+ return recordContractLinesMoney(lines, {
310
+ currency: offer.priceBreakdown.currency,
311
+ taxRate: offer.priceBreakdown.vatRate
312
+ });
152
313
  }
153
314
  offerLineItemToContractLineItem(item) {
154
315
  return {
@@ -160,7 +321,6 @@ var SubscriptionContractService = class {
160
321
  quantity: item.quantity,
161
322
  unit: item.unit ?? null,
162
323
  priceNet: item.priceNet,
163
- priceGross: item.priceGross,
164
324
  billingCycle: item.billingCycle,
165
325
  minimumTermUntil: this.parseOptionalDate(item.minimumTermUntil, "lineItems.minimumTermUntil"),
166
326
  featuresSnapshot: [
@@ -192,7 +352,7 @@ var SubscriptionContractService = class {
192
352
  currency: breakdown.currency,
193
353
  billingCycle: breakdown.billingCycle,
194
354
  subtotalNet: breakdown.regularNet,
195
- discountNet: Math.max(0, breakdown.regularNet - breakdown.effectiveNet),
355
+ discountNet: Math.max(0, round2(breakdown.regularNet - breakdown.effectiveNet)),
196
356
  totalNet: breakdown.effectiveNet,
197
357
  vatRate: breakdown.vatRate,
198
358
  totalGross: breakdown.effectiveGross
@@ -205,19 +365,36 @@ var SubscriptionContractService = class {
205
365
  message: "A subscription contract requires at least one line item."
206
366
  });
207
367
  }
208
- if (data.effectiveUntil && data.effectiveUntil <= data.effectiveFrom) {
368
+ assertContractWindow(data.effectiveFrom, data.effectiveUntil ?? null);
369
+ assertOnePlanLine(data.lineItems);
370
+ assertTaxRatePercent("priceSnapshot.vatRate", data.priceSnapshot.vatRate);
371
+ data.lineItems.forEach((item, index) => assertTaxRatePercent(`lineItems[${index}].taxRate`, item.taxRate));
372
+ const foreign = data.lineItems.find((item) => item.currency !== data.priceSnapshot.currency);
373
+ if (foreign) {
209
374
  throw new UnprocessableEntityException({
210
- code: CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_INVALID_WINDOW,
211
- message: "effectiveUntil must be after effectiveFrom."
375
+ code: CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_LINE_ITEM_CURRENCY_MISMATCH,
376
+ message: "A line item must be booked in the currency its contract was priced in.",
377
+ params: {
378
+ sourceKey: foreign.sourceKey,
379
+ currency: foreign.currency,
380
+ expected: data.priceSnapshot.currency
381
+ }
212
382
  });
213
383
  }
214
- const planLineItems = data.lineItems.filter((item) => item.kind === "plan");
215
- if (planLineItems.length !== 1) {
384
+ const contradictory = data.lineItems.find((item) => round2(item.priceGross - item.priceNet) !== round2(item.taxAmount));
385
+ if (contradictory) {
216
386
  throw new UnprocessableEntityException({
217
- code: CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_PLAN_LINE_ITEM_REQUIRED,
218
- message: "A subscription contract requires exactly one plan base item."
387
+ code: CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_LINE_ITEM_TAX_MISMATCH,
388
+ message: "A line item's taxAmount must be exactly priceGross minus priceNet.",
389
+ params: {
390
+ sourceKey: contradictory.sourceKey,
391
+ taxAmount: contradictory.taxAmount,
392
+ expected: round2(contradictory.priceGross - contradictory.priceNet)
393
+ }
219
394
  });
220
395
  }
396
+ assertLinesAddUp(data.lineItems, data.priceSnapshot);
397
+ assertNoNegativeDiscount(data);
221
398
  }
222
399
  assertTerminable(existing, data) {
223
400
  if (existing.status === "terminated" || existing.status === "superseded") {
@@ -273,9 +450,11 @@ var SubscriptionContractService = class {
273
450
  SubscriptionContractService = _ts_decorate([
274
451
  Injectable(),
275
452
  _ts_param(0, Inject(SUBSCRIPTION_CONTRACT_REPOSITORY_TOKEN)),
453
+ _ts_param(1, Inject(SubscriberService)),
276
454
  _ts_metadata("design:type", Function),
277
455
  _ts_metadata("design:paramtypes", [
278
- typeof SubscriptionContractRepository === "undefined" ? Object : SubscriptionContractRepository
456
+ typeof SubscriptionContractRepository === "undefined" ? Object : SubscriptionContractRepository,
457
+ typeof SubscriberService === "undefined" ? Object : SubscriberService
279
458
  ])
280
459
  ], SubscriptionContractService);
281
460
  function contractLineItemToInvoiceLineItem(item) {
@@ -291,6 +470,9 @@ function contractLineItemToInvoiceLineItem(item) {
291
470
  priceNet: item.priceNet,
292
471
  priceGross: item.priceGross,
293
472
  billingCycle: item.billingCycle,
473
+ currency: item.currency,
474
+ taxRate: item.taxRate,
475
+ taxAmount: item.taxAmount,
294
476
  minimumTermUntil: item.minimumTermUntil,
295
477
  metadata: item.metadata
296
478
  };
@@ -299,7 +481,6 @@ __name(contractLineItemToInvoiceLineItem, "contractLineItemToInvoiceLineItem");
299
481
  function subscriptionContractToInvoiceSnapshot(contract) {
300
482
  return {
301
483
  contractId: contract.id,
302
- projectKey: contract.projectKey,
303
484
  tenantId: contract.tenantId,
304
485
  originalOfferId: contract.originalOfferId,
305
486
  currency: contract.priceSnapshot.currency,
@@ -334,6 +515,7 @@ function lineItemKindPriority(kind) {
334
515
  __name(lineItemKindPriority, "lineItemKindPriority");
335
516
 
336
517
  export {
518
+ appendImplicitDiscountLineItem,
337
519
  SubscriptionContractService,
338
520
  contractLineItemToInvoiceLineItem,
339
521
  subscriptionContractToInvoiceSnapshot,
@@ -1,12 +1,10 @@
1
+ import {
2
+ AUDIT_PORT_TOKEN
3
+ } from "./chunk-SZ7RFPXA.js";
1
4
  import {
2
5
  __name
3
6
  } from "./chunk-SHUYVCID.js";
4
7
 
5
- // src/admin/admin.tokens.ts
6
- var MFA_PORT_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/nest/MfaPort");
7
- var AUDIT_PORT_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/nest/AuditPort");
8
- var RLS_BYPASS_PORT_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/nest/RlsBypassPort");
9
-
10
8
  // src/admin/admin-audit.service.ts
11
9
  import * as os from "os";
12
10
  import { Inject, Injectable } from "@nestjs/common";
@@ -83,8 +81,5 @@ AdminAuditService = _ts_decorate([
83
81
  ], AdminAuditService);
84
82
 
85
83
  export {
86
- MFA_PORT_TOKEN,
87
- AUDIT_PORT_TOKEN,
88
- RLS_BYPASS_PORT_TOKEN,
89
84
  AdminAuditService
90
85
  };