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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/README.md +44 -8
  2. package/dist/.build-stamp +1 -1
  3. package/dist/_entries.cjs +10214 -4796
  4. package/dist/admin/index.d.cts +5 -4
  5. package/dist/admin/index.d.ts +5 -4
  6. package/dist/admin/index.js +13 -9
  7. package/dist/{admin-resources.module-CpvzcfCc.d.cts → admin-resources.module-B4XNvdH-.d.cts} +1 -2
  8. package/dist/{admin-resources.module-DF5_Q7RD.d.ts → admin-resources.module-CWqdI9BV.d.ts} +1 -2
  9. package/dist/{admin-stats.service-TYiSUwIR.d.ts → admin-stats.service-CsLNt19Z.d.ts} +11 -7
  10. package/dist/{admin-stats.service-DfKupywR.d.cts → admin-stats.service-DrZrU8IJ.d.cts} +11 -7
  11. package/dist/{aggregation-B3CD0v_d.d.cts → aggregation-ChpZDh0g.d.ts} +39 -6
  12. package/dist/{aggregation-BFzW07DE.d.ts → aggregation-OpnhwzF5.d.cts} +39 -6
  13. package/dist/billing/index.d.cts +448 -108
  14. package/dist/billing/index.d.ts +448 -108
  15. package/dist/billing/index.js +108 -41
  16. package/dist/catalog/index.d.cts +74 -45
  17. package/dist/catalog/index.d.ts +74 -45
  18. package/dist/catalog/index.js +21 -13
  19. package/dist/catalog.module-C9O7BxRq.d.ts +180 -0
  20. package/dist/catalog.module-T-I5Ricz.d.cts +180 -0
  21. package/dist/checkout-offer/index.d.cts +17 -22
  22. package/dist/checkout-offer/index.d.ts +17 -22
  23. package/dist/checkout-offer/index.js +21 -7
  24. package/dist/checkout-offer.module-DFhGy6JO.d.cts +58 -0
  25. package/dist/checkout-offer.module-x7SP_dX4.d.ts +58 -0
  26. package/dist/checkout-offer.service-BeHgYikI.d.ts +242 -0
  27. package/dist/checkout-offer.service-CCVxG9b2.d.cts +242 -0
  28. package/dist/chunk-34XVPR6P.js +222 -0
  29. package/dist/{chunk-AOQJEYOL.js → chunk-3QVGA6JX.js} +3 -1
  30. package/dist/chunk-3RLWML2S.js +896 -0
  31. package/dist/{chunk-TXC3LHHB.js → chunk-4SDSYV4A.js} +21 -13
  32. package/dist/chunk-55QEDAMX.js +946 -0
  33. package/dist/chunk-5CKC7O52.js +140 -0
  34. package/dist/chunk-6PJE7EWO.js +185 -0
  35. package/dist/chunk-7NJKVC5X.js +353 -0
  36. package/dist/{chunk-RUNZ3X4C.js → chunk-AOS2JPDD.js} +1263 -185
  37. package/dist/{chunk-WOVQPXV4.js → chunk-CQ2ZZMTD.js} +702 -725
  38. package/dist/{chunk-NA6O63B7.js → chunk-F5X66HF5.js} +5 -5
  39. package/dist/chunk-FEQZCWUL.js +174 -0
  40. package/dist/{chunk-VIJ5NJWC.js → chunk-G6EZWECL.js} +107 -31
  41. package/dist/chunk-G7RQO2XO.js +0 -0
  42. package/dist/{chunk-N3L3AICU.js → chunk-GPQWGA6B.js} +5 -6
  43. package/dist/chunk-GZTL64QK.js +7 -0
  44. package/dist/{chunk-SABTXESR.js → chunk-J7NJRK3K.js} +17 -1
  45. package/dist/{chunk-SEPN52AD.js → chunk-JBCC6C3M.js} +566 -307
  46. package/dist/{chunk-I7GTA3RX.js → chunk-JQRA724W.js} +1 -1
  47. package/dist/{chunk-WXYJHZCN.js → chunk-JVAEKTJ4.js} +2 -14
  48. package/dist/chunk-KPEMTBOP.js +26 -0
  49. package/dist/chunk-LF6J4YYN.js +301 -0
  50. package/dist/{chunk-LTT736P3.js → chunk-LLYVYRGJ.js} +228 -387
  51. package/dist/{chunk-6Z7JR4EW.js → chunk-M47BNEY2.js} +1897 -633
  52. package/dist/{chunk-KFT5AIIH.js → chunk-NDLC5GYK.js} +206 -24
  53. package/dist/{chunk-O2J2HDXA.js → chunk-OOTEXP47.js} +3 -8
  54. package/dist/chunk-QPVDCKYS.js +702 -0
  55. package/dist/{chunk-NHVDCYK5.js → chunk-R5YCJBHY.js} +71 -106
  56. package/dist/{chunk-XBYAFEOR.js → chunk-S33SO5XX.js} +1 -1
  57. package/dist/chunk-SZ7RFPXA.js +10 -0
  58. package/dist/chunk-TBBZWZQT.js +98 -0
  59. package/dist/chunk-TOEFN7DN.js +295 -0
  60. package/dist/{chunk-7ZEGFL42.js → chunk-WHJSYKNC.js} +6 -6
  61. package/dist/chunk-WOEJ6K7M.js +55 -0
  62. package/dist/{chunk-AU3OOREM.js → chunk-XSSWYPP5.js} +16 -3
  63. package/dist/contract-line-item-money-B0Z3Ogur.d.cts +43 -0
  64. package/dist/contract-line-item-money-B0Z3Ogur.d.ts +43 -0
  65. package/dist/discovery/index.d.cts +2 -2
  66. package/dist/discovery/index.d.ts +2 -2
  67. package/dist/discovery/index.js +6 -6
  68. package/dist/{discovery.scanner-9cMqF95o.d.cts → discovery.scanner-DXKc6JkV.d.cts} +1 -1
  69. package/dist/{discovery.scanner-9cMqF95o.d.ts → discovery.scanner-DXKc6JkV.d.ts} +1 -1
  70. package/dist/{enforce-quota.interceptor-Df0e3OWe.d.cts → enforce-quota.interceptor-BFyVdWe8.d.cts} +6 -6
  71. package/dist/{enforce-quota.interceptor-DW3etVyI.d.ts → enforce-quota.interceptor-GaBXboGx.d.ts} +6 -6
  72. package/dist/entitlement/index.d.cts +27 -4
  73. package/dist/entitlement/index.d.ts +27 -4
  74. package/dist/entitlement/index.js +13 -8
  75. package/dist/index.d.cts +209 -28
  76. package/dist/index.d.ts +209 -28
  77. package/dist/index.js +285 -163
  78. package/dist/issuer-identity.check-CBO0vcsq.d.cts +197 -0
  79. package/dist/issuer-identity.check-Cz9CEMkZ.d.ts +197 -0
  80. package/dist/{module-options-COSEb0VW.d.ts → module-options-DmQ3G4sZ.d.ts} +135 -41
  81. package/dist/{module-options-BG3MzQva.d.cts → module-options-qAOwinHR.d.cts} +135 -41
  82. package/dist/payment-callback.service-CR84Xw1Z.d.cts +125 -0
  83. package/dist/payment-callback.service-CR84Xw1Z.d.ts +125 -0
  84. package/dist/payments/index.cjs +4 -0
  85. package/dist/payments/index.d.cts +187 -0
  86. package/dist/payments/index.d.ts +187 -0
  87. package/dist/payments/index.js +141 -0
  88. package/dist/payments.module-BdUq9ot_.d.cts +57 -0
  89. package/dist/payments.module-CQGv_VG7.d.ts +57 -0
  90. package/dist/plan-catalog-source-DWe-BGY1.d.cts +21 -0
  91. package/dist/plan-catalog-source-DWe-BGY1.d.ts +21 -0
  92. package/dist/{plan-resolution-Cgo_TR1H.d.cts → plan-resolution-CwUy_OqC.d.cts} +14 -0
  93. package/dist/{plan-resolution-Cgo_TR1H.d.ts → plan-resolution-CwUy_OqC.d.ts} +14 -0
  94. package/dist/{plan-versions.service-DChhbK6h.d.ts → plan-versions.service-C0N-Em2W.d.ts} +38 -64
  95. package/dist/{plan-versions.service-TPEtbN0r.d.cts → plan-versions.service-DARLh-qI.d.cts} +38 -64
  96. package/dist/platform/index.d.cts +52 -23
  97. package/dist/platform/index.d.ts +52 -23
  98. package/dist/platform/index.js +57 -36
  99. package/dist/promo/index.d.cts +23 -8
  100. package/dist/promo/index.d.ts +23 -8
  101. package/dist/promo/index.js +15 -7
  102. package/dist/{promo.module-DtvIycpt.d.cts → promo.module-CKkECmU8.d.cts} +9 -3
  103. package/dist/{promo.module-Z8h1PNX-.d.ts → promo.module-DuIJdK3U.d.ts} +9 -3
  104. package/dist/promo.service-BYBu6dy3.d.cts +215 -0
  105. package/dist/promo.service-CFjfF0Xm.d.ts +215 -0
  106. package/dist/registration/index.d.cts +104 -47
  107. package/dist/registration/index.d.ts +104 -47
  108. package/dist/registration/index.js +15 -9
  109. package/dist/subscriber/index.cjs +4 -0
  110. package/dist/subscriber/index.d.cts +28 -0
  111. package/dist/subscriber/index.d.ts +28 -0
  112. package/dist/subscriber/index.js +16 -0
  113. package/dist/subscriber.service-5W6Gi-1B.d.cts +68 -0
  114. package/dist/subscriber.service-5W6Gi-1B.d.ts +68 -0
  115. package/dist/subscription-contract/index.d.cts +4 -2
  116. package/dist/subscription-contract/index.d.ts +4 -2
  117. package/dist/subscription-contract/index.js +13 -5
  118. package/dist/{subscription-contract.module-fDFdQ0jm.d.ts → subscription-contract.module-BfIRpc1o.d.ts} +8 -2
  119. package/dist/{subscription-contract.module-CEK8HS5-.d.cts → subscription-contract.module-DTMuUJYd.d.cts} +8 -2
  120. package/dist/{subscription-contract.service-hD87MKyg.d.cts → subscription-contract.service-CPZrTM9C.d.ts} +32 -3
  121. package/dist/{subscription-contract.service-hD87MKyg.d.ts → subscription-contract.service-CfNApLuP.d.cts} +32 -3
  122. package/dist/tenant-billing.controller-BLqbsONe.d.ts +812 -0
  123. package/dist/tenant-billing.controller-DKbAqTbM.d.cts +812 -0
  124. package/dist/tenant-billing.module-ClbjzocC.d.cts +364 -0
  125. package/dist/tenant-billing.module-DohgJJ2m.d.ts +364 -0
  126. package/dist/tenant-billing.tokens-G-1xOlMr.d.cts +129 -0
  127. package/dist/tenant-billing.tokens-G-1xOlMr.d.ts +129 -0
  128. package/dist/testing/index.d.cts +44 -17
  129. package/dist/testing/index.d.ts +44 -17
  130. package/dist/testing/index.js +189 -40
  131. package/package.json +24 -4
  132. package/dist/catalog.module-BDDO6iwq.d.cts +0 -103
  133. package/dist/catalog.module-BTnMsE6u.d.ts +0 -103
  134. package/dist/checkout-offer.module-CMtiQJTT.d.ts +0 -39
  135. package/dist/checkout-offer.module-CVpWrzbe.d.cts +0 -39
  136. package/dist/checkout-offer.service-BLOv2HOo.d.cts +0 -49
  137. package/dist/checkout-offer.service-BLOv2HOo.d.ts +0 -49
  138. package/dist/chunk-57V6ZTI6.js +0 -22
  139. package/dist/chunk-BSK6YBLI.js +0 -87
  140. package/dist/chunk-DQKCK7DX.js +0 -217
  141. package/dist/chunk-VXEYLNIB.js +0 -632
  142. package/dist/chunk-WUDYIPYH.js +0 -715
  143. package/dist/define-saasicat-DROBe-b1.d.ts +0 -69
  144. package/dist/define-saasicat-vGBCkFys.d.cts +0 -69
  145. package/dist/promo.service-BJbKAmw3.d.cts +0 -112
  146. package/dist/promo.service-BJbKAmw3.d.ts +0 -112
  147. package/dist/tenant-billing.controller-BQ67mlmI.d.ts +0 -438
  148. package/dist/tenant-billing.controller-DS8kB8a7.d.cts +0 -438
  149. package/dist/tenant-billing.module-BznC3XtW.d.cts +0 -299
  150. package/dist/tenant-billing.module-wUbsQSFG.d.ts +0 -299
  151. /package/dist/{chunk-2FR6ZL7R.js → chunk-BHZOH2DY.js} +0 -0
  152. /package/dist/{chunk-2SLTRKXC.js → chunk-DFOW3JVO.js} +0 -0
  153. /package/dist/{chunk-DBZGV3HC.js → chunk-DU56UKEH.js} +0 -0
  154. /package/dist/{chunk-OYNHOY45.js → chunk-GAUEVIAE.js} +0 -0
@@ -0,0 +1,140 @@
1
+ import {
2
+ BILLING_PERMISSION_GUARDS_TOKEN,
3
+ TENANT_AUTH_GUARDS_TOKEN
4
+ } from "./chunk-GPQWGA6B.js";
5
+ import {
6
+ codedError
7
+ } from "./chunk-J7NJRK3K.js";
8
+ import {
9
+ __name
10
+ } from "./chunk-SHUYVCID.js";
11
+
12
+ // src/billing/composed-tenant-auth.guard.ts
13
+ import { ForbiddenException, Inject, Injectable, Optional } from "@nestjs/common";
14
+ import { AUTH_ERROR_CODES } from "@saasicat/core";
15
+ function _ts_decorate(decorators, target, key, desc) {
16
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
17
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
18
+ 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;
19
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
20
+ }
21
+ __name(_ts_decorate, "_ts_decorate");
22
+ function _ts_metadata(k, v) {
23
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
24
+ }
25
+ __name(_ts_metadata, "_ts_metadata");
26
+ function _ts_param(paramIndex, decorator) {
27
+ return function(target, key) {
28
+ decorator(target, key, paramIndex);
29
+ };
30
+ }
31
+ __name(_ts_param, "_ts_param");
32
+ var ComposedTenantAuthGuard = class {
33
+ static {
34
+ __name(this, "ComposedTenantAuthGuard");
35
+ }
36
+ guards;
37
+ constructor(guards = null) {
38
+ this.guards = guards;
39
+ }
40
+ async canActivate(context) {
41
+ if (!this.guards || this.guards.length === 0) {
42
+ throw new ForbiddenException({
43
+ code: AUTH_ERROR_CODES.AUTH_GUARDS_NOT_CONFIGURED,
44
+ message: "TenantBillingModule.forRoot.authGuards is not configured."
45
+ });
46
+ }
47
+ for (const guard of this.guards) {
48
+ const result = await Promise.resolve(guard.canActivate(context));
49
+ if (!result) return false;
50
+ }
51
+ return true;
52
+ }
53
+ };
54
+ ComposedTenantAuthGuard = _ts_decorate([
55
+ Injectable(),
56
+ _ts_param(0, Optional()),
57
+ _ts_param(0, Inject(TENANT_AUTH_GUARDS_TOKEN)),
58
+ _ts_metadata("design:type", Function),
59
+ _ts_metadata("design:paramtypes", [
60
+ Object
61
+ ])
62
+ ], ComposedTenantAuthGuard);
63
+
64
+ // src/billing/billing-permission.guard.ts
65
+ import { ForbiddenException as ForbiddenException3, Inject as Inject2, Injectable as Injectable2, Optional as Optional2 } from "@nestjs/common";
66
+ import { AUTH_ERROR_CODES as AUTH_ERROR_CODES3 } from "@saasicat/core";
67
+
68
+ // src/billing/tenant-roles.ts
69
+ import { ForbiddenException as ForbiddenException2 } from "@nestjs/common";
70
+ import { AUTH_ERROR_CODES as AUTH_ERROR_CODES2 } from "@saasicat/core";
71
+ var ADMIN_ROLES = /* @__PURE__ */ new Set([
72
+ "TENANT_ADMIN",
73
+ "SUPER_ADMIN"
74
+ ]);
75
+ function holdsTenantAdminRole(context) {
76
+ const request = context.switchToHttp().getRequest();
77
+ if (!request.user) {
78
+ throw new ForbiddenException2({
79
+ code: AUTH_ERROR_CODES2.NOT_AUTHENTICATED,
80
+ message: "Not authenticated"
81
+ });
82
+ }
83
+ const role = request.user.platformRole ?? request.user.role;
84
+ return Boolean(role && ADMIN_ROLES.has(role));
85
+ }
86
+ __name(holdsTenantAdminRole, "holdsTenantAdminRole");
87
+
88
+ // src/billing/billing-permission.guard.ts
89
+ function _ts_decorate2(decorators, target, key, desc) {
90
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
91
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
92
+ 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;
93
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
94
+ }
95
+ __name(_ts_decorate2, "_ts_decorate");
96
+ function _ts_metadata2(k, v) {
97
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
98
+ }
99
+ __name(_ts_metadata2, "_ts_metadata");
100
+ function _ts_param2(paramIndex, decorator) {
101
+ return function(target, key) {
102
+ decorator(target, key, paramIndex);
103
+ };
104
+ }
105
+ __name(_ts_param2, "_ts_param");
106
+ var BillingPermissionGuard = class {
107
+ static {
108
+ __name(this, "BillingPermissionGuard");
109
+ }
110
+ guards;
111
+ constructor(guards = null) {
112
+ this.guards = guards;
113
+ }
114
+ async canActivate(context) {
115
+ if (await this.holdsPermission(context)) return true;
116
+ throw new ForbiddenException3(codedError(AUTH_ERROR_CODES3.BILLING_PERMISSION_REQUIRED));
117
+ }
118
+ async holdsPermission(context) {
119
+ if (!this.guards || this.guards.length === 0) return holdsTenantAdminRole(context);
120
+ for (const guard of this.guards) {
121
+ if (!await Promise.resolve(guard.canActivate(context))) return false;
122
+ }
123
+ return true;
124
+ }
125
+ };
126
+ BillingPermissionGuard = _ts_decorate2([
127
+ Injectable2(),
128
+ _ts_param2(0, Optional2()),
129
+ _ts_param2(0, Inject2(BILLING_PERMISSION_GUARDS_TOKEN)),
130
+ _ts_metadata2("design:type", Function),
131
+ _ts_metadata2("design:paramtypes", [
132
+ Object
133
+ ])
134
+ ], BillingPermissionGuard);
135
+
136
+ export {
137
+ ComposedTenantAuthGuard,
138
+ holdsTenantAdminRole,
139
+ BillingPermissionGuard
140
+ };
@@ -0,0 +1,185 @@
1
+ import {
2
+ __name
3
+ } from "./chunk-SHUYVCID.js";
4
+
5
+ // src/promo/math.ts
6
+ function round2(n) {
7
+ return Math.round(n * 100) / 100;
8
+ }
9
+ __name(round2, "round2");
10
+ function isTaxRatePercent(rate) {
11
+ return Number.isFinite(rate) && rate >= 0 && rate <= 100 && !(rate > 0 && rate < 1);
12
+ }
13
+ __name(isTaxRatePercent, "isTaxRatePercent");
14
+ function grossFromNet(net, vatRate) {
15
+ return round2(net * (1 + vatRate / 100));
16
+ }
17
+ __name(grossFromNet, "grossFromNet");
18
+ function netFromGross(gross, vatRate) {
19
+ return round2(gross * 100 / (100 + vatRate));
20
+ }
21
+ __name(netFromGross, "netFromGross");
22
+ function computeIncludedVat(grossAmount, vatRate) {
23
+ return round2(grossAmount * vatRate / (100 + vatRate));
24
+ }
25
+ __name(computeIncludedVat, "computeIncludedVat");
26
+
27
+ // src/subscription-contract/contract-line-item-money.ts
28
+ var MONTHS_PER_YEAR = 12;
29
+ function recordContractLinesMoney(lines, money) {
30
+ const shares = grossSharesOf(lines, money.taxRate);
31
+ return lines.map((line, index) => {
32
+ const priceGross = shares[index];
33
+ return {
34
+ ...line,
35
+ priceGross,
36
+ currency: money.currency,
37
+ taxRate: money.taxRate,
38
+ taxAmount: fromCents(toCents(priceGross) - toCents(line.priceNet))
39
+ };
40
+ });
41
+ }
42
+ __name(recordContractLinesMoney, "recordContractLinesMoney");
43
+ function grossSharesOf(lines, taxRate) {
44
+ const netSoFar = /* @__PURE__ */ new Map();
45
+ return lines.map((line) => {
46
+ const before = netSoFar.get(line.billingCycle) ?? 0;
47
+ const after = before + toCents(line.priceNet);
48
+ netSoFar.set(line.billingCycle, after);
49
+ return fromCents(grossCents(after, taxRate) - grossCents(before, taxRate));
50
+ });
51
+ }
52
+ __name(grossSharesOf, "grossSharesOf");
53
+ function contractTotalsOf(lines, contractCycle) {
54
+ let subtotalNet = 0;
55
+ let discountNet = 0;
56
+ let totalGross = 0;
57
+ for (const line of lines) {
58
+ const times = timesDueInOnePeriod(contractCycle, line.billingCycle);
59
+ const net = times * toCents(line.priceNet);
60
+ if (line.kind === "discount") discountNet -= net;
61
+ else subtotalNet += net;
62
+ totalGross += times * toCents(line.priceGross);
63
+ }
64
+ return {
65
+ subtotalNet: fromCents(subtotalNet),
66
+ discountNet: fromCents(discountNet),
67
+ totalNet: fromCents(subtotalNet - discountNet),
68
+ totalGross: fromCents(totalGross)
69
+ };
70
+ }
71
+ __name(contractTotalsOf, "contractTotalsOf");
72
+ function timesDueInOnePeriod(contractCycle, lineCycle) {
73
+ return contractCycle === "yearly" && lineCycle === "monthly" ? MONTHS_PER_YEAR : 1;
74
+ }
75
+ __name(timesDueInOnePeriod, "timesDueInOnePeriod");
76
+ function grossCents(netCents, taxRate) {
77
+ return toCents(grossFromNet(fromCents(netCents), taxRate));
78
+ }
79
+ __name(grossCents, "grossCents");
80
+ function toCents(amount) {
81
+ return Math.round(amount * 100);
82
+ }
83
+ __name(toCents, "toCents");
84
+ function fromCents(cents) {
85
+ return cents === 0 ? 0 : cents / 100;
86
+ }
87
+ __name(fromCents, "fromCents");
88
+
89
+ // src/subscription-contract/contract-refusals.ts
90
+ import { UnprocessableEntityException } from "@nestjs/common";
91
+ import { CONTRACT_ERROR_CODES } from "@saasicat/core";
92
+ function assertTaxRatePercent(field, taxRate) {
93
+ if (isTaxRatePercent(taxRate)) return;
94
+ throw new UnprocessableEntityException({
95
+ code: CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_TAX_RATE_NOT_PERCENT,
96
+ message: `A subscription contract states a tax rate of ${taxRate} at ${field}, which is not a percentage: a rate lies from 0 to 100, and a value between 0 and 1 is refused as a fraction.`,
97
+ params: {
98
+ field,
99
+ taxRate
100
+ }
101
+ });
102
+ }
103
+ __name(assertTaxRatePercent, "assertTaxRatePercent");
104
+ function assertContractWindow(effectiveFrom, effectiveUntil) {
105
+ if (!effectiveUntil || effectiveUntil > effectiveFrom) return;
106
+ throw new UnprocessableEntityException({
107
+ code: CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_INVALID_WINDOW,
108
+ message: "effectiveUntil must be after effectiveFrom."
109
+ });
110
+ }
111
+ __name(assertContractWindow, "assertContractWindow");
112
+ function assertOnePlanLine(lineItems) {
113
+ if (lineItems.filter((item) => item.kind === "plan").length === 1) return;
114
+ throw new UnprocessableEntityException({
115
+ code: CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_PLAN_LINE_ITEM_REQUIRED,
116
+ message: "A subscription contract requires exactly one plan base item."
117
+ });
118
+ }
119
+ __name(assertOnePlanLine, "assertOnePlanLine");
120
+ var ADDED_UP_TOTALS = [
121
+ "subtotalNet",
122
+ "discountNet",
123
+ "totalNet",
124
+ "totalGross"
125
+ ];
126
+ function assertLinesAddUp(lineItems, priceSnapshot) {
127
+ const lines = contractTotalsOf(lineItems, priceSnapshot.billingCycle);
128
+ const field = ADDED_UP_TOTALS.find((total) => Math.round(lines[total] * 100) !== Math.round(priceSnapshot[total] * 100));
129
+ if (!field) return;
130
+ throw new UnprocessableEntityException({
131
+ code: CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_LINES_DO_NOT_ADD_UP,
132
+ message: `The line items of a subscription contract add up to ${lines[field]} for priceSnapshot.${field}, but the contract states ${priceSnapshot[field]}.`,
133
+ params: {
134
+ field: `priceSnapshot.${field}`,
135
+ lines: lines[field],
136
+ stated: priceSnapshot[field]
137
+ }
138
+ });
139
+ }
140
+ __name(assertLinesAddUp, "assertLinesAddUp");
141
+ function assertNoNegativeDiscount(data) {
142
+ const negative = (data.priceSnapshot.discountNet < 0 ? {
143
+ field: "priceSnapshot.discountNet",
144
+ amount: data.priceSnapshot.discountNet
145
+ } : null) ?? negativeResolvedAmount("promotionSnapshots", data.promotionSnapshots) ?? negativeResolvedAmount("promoCodeSnapshots", data.promoCodeSnapshots);
146
+ if (!negative) return;
147
+ throw new UnprocessableEntityException({
148
+ code: CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_DISCOUNT_NEGATIVE,
149
+ message: `A subscription contract states a discount of ${negative.amount} at ${negative.field}; a discount takes money off and is never negative.`,
150
+ params: {
151
+ field: negative.field,
152
+ amount: negative.amount
153
+ }
154
+ });
155
+ }
156
+ __name(assertNoNegativeDiscount, "assertNoNegativeDiscount");
157
+ function negativeResolvedAmount(list, snapshots) {
158
+ for (const [index, snapshot] of (snapshots ?? []).entries()) {
159
+ const amount = snapshot?.resolvedAmountNet;
160
+ if (typeof amount === "number" && amount < 0) {
161
+ return {
162
+ field: `${list}[${index}].resolvedAmountNet`,
163
+ amount
164
+ };
165
+ }
166
+ }
167
+ return null;
168
+ }
169
+ __name(negativeResolvedAmount, "negativeResolvedAmount");
170
+
171
+ export {
172
+ round2,
173
+ isTaxRatePercent,
174
+ grossFromNet,
175
+ netFromGross,
176
+ computeIncludedVat,
177
+ recordContractLinesMoney,
178
+ grossSharesOf,
179
+ contractTotalsOf,
180
+ assertTaxRatePercent,
181
+ assertContractWindow,
182
+ assertOnePlanLine,
183
+ assertLinesAddUp,
184
+ assertNoNegativeDiscount
185
+ };
@@ -0,0 +1,353 @@
1
+ import {
2
+ PLAN_CATALOG_SETTINGS_TOKEN
3
+ } from "./chunk-FEQZCWUL.js";
4
+ import {
5
+ codedError
6
+ } from "./chunk-J7NJRK3K.js";
7
+ import {
8
+ __name
9
+ } from "./chunk-SHUYVCID.js";
10
+
11
+ // src/payments/payments.tokens.ts
12
+ var PAYMENT_GATEWAYS_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/nest/PaymentGateways");
13
+ var PAYMENT_EVENT_LOG_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/nest/PaymentEventLog");
14
+ var SUBSCRIBER_PAYMENT_METHOD_REPOSITORY_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/nest/SubscriberPaymentMethodRepository");
15
+ var PAYMENT_TRANSACTION_RUNNER_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/nest/PaymentTransactionRunner");
16
+
17
+ // src/payments/payment-gateway-registry.ts
18
+ import { Inject, Injectable } from "@nestjs/common";
19
+
20
+ // src/payments/payment-accounts.ts
21
+ function paymentAccountProblems(payments, gateways) {
22
+ const bound = Object.keys(gateways);
23
+ if (!payments) {
24
+ return [
25
+ `Payment gateways are bound for ${bound.join(", ") || "no account"}, but config/saas.yaml has no \`payments\` block naming the accounts.`
26
+ ];
27
+ }
28
+ const named = Object.keys(payments.accounts);
29
+ const problems = [];
30
+ for (const account of named) {
31
+ const gateway = gateways[account];
32
+ if (!gateway) {
33
+ problems.push(`config/saas.yaml names the payment account '${account}', and no gateway is bound for it: bind its adapter, or remove the account once it holds nothing.`);
34
+ continue;
35
+ }
36
+ const provider = payments.accounts[account].provider;
37
+ if (gateway.provider !== provider) {
38
+ problems.push(`The payment account '${account}' is at '${provider}' in config/saas.yaml, but the gateway bound for it is '${gateway.provider}'.`);
39
+ }
40
+ }
41
+ for (const account of bound) {
42
+ if (!named.includes(account)) {
43
+ problems.push(`A gateway is bound for the payment account '${account}', which config/saas.yaml#payments.accounts does not name.`);
44
+ }
45
+ }
46
+ const takesNew = payments.newPaymentMethods;
47
+ if (takesNew !== void 0) {
48
+ const account = payments.accounts[takesNew];
49
+ if (!account) {
50
+ problems.push(`config/saas.yaml#payments.newPaymentMethods names '${takesNew}', which is not one of its accounts.`);
51
+ } else if (!account.methods?.length) {
52
+ problems.push(`The payment account '${takesNew}' takes new payment methods, and lists no \`methods\` to offer.`);
53
+ }
54
+ }
55
+ return problems;
56
+ }
57
+ __name(paymentAccountProblems, "paymentAccountProblems");
58
+
59
+ // src/payments/payment-gateway-registry.ts
60
+ function _ts_decorate(decorators, target, key, desc) {
61
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
62
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
63
+ 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;
64
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
65
+ }
66
+ __name(_ts_decorate, "_ts_decorate");
67
+ function _ts_metadata(k, v) {
68
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
69
+ }
70
+ __name(_ts_metadata, "_ts_metadata");
71
+ function _ts_param(paramIndex, decorator) {
72
+ return function(target, key) {
73
+ decorator(target, key, paramIndex);
74
+ };
75
+ }
76
+ __name(_ts_param, "_ts_param");
77
+ var PaymentGatewayRegistry = class {
78
+ static {
79
+ __name(this, "PaymentGatewayRegistry");
80
+ }
81
+ settings;
82
+ accounts = /* @__PURE__ */ new Map();
83
+ takesNew;
84
+ constructor(settings, gateways) {
85
+ this.settings = settings;
86
+ const problems = paymentAccountProblems(settings.payments, gateways);
87
+ if (problems.length > 0) {
88
+ throw new Error(`Payments cannot start:
89
+ - ${problems.join("\n- ")}`);
90
+ }
91
+ for (const [name, account] of Object.entries(settings.payments.accounts)) {
92
+ this.accounts.set(name, {
93
+ name,
94
+ provider: account.provider,
95
+ gateway: gateways[name]
96
+ });
97
+ }
98
+ this.takesNew = settings.payments.newPaymentMethods;
99
+ }
100
+ /** The account a callback names, or `null` when none is configured under it. */
101
+ account(name) {
102
+ return this.accounts.get(name) ?? null;
103
+ }
104
+ /** The accounts among `held` that are not configured, each once — references nothing can reach any more. */
105
+ unconfigured(held) {
106
+ return [
107
+ ...new Set(held)
108
+ ].filter((name) => !this.accounts.has(name));
109
+ }
110
+ /** The origins a gateway's form may send a person back to. */
111
+ returnUrlOrigins() {
112
+ return this.settings.payments.returnUrlOrigins;
113
+ }
114
+ /** The account a new payment method is taken at, or `null` when none takes new ones. */
115
+ forNewPaymentMethods() {
116
+ if (this.takesNew === void 0) return null;
117
+ const account = this.accounts.get(this.takesNew);
118
+ return {
119
+ ...account,
120
+ methods: this.settings.payments.accounts[this.takesNew].methods
121
+ };
122
+ }
123
+ };
124
+ PaymentGatewayRegistry = _ts_decorate([
125
+ Injectable(),
126
+ _ts_param(0, Inject(PLAN_CATALOG_SETTINGS_TOKEN)),
127
+ _ts_param(1, Inject(PAYMENT_GATEWAYS_TOKEN)),
128
+ _ts_metadata("design:type", Function),
129
+ _ts_metadata("design:paramtypes", [
130
+ typeof PlanCatalogSettings === "undefined" ? Object : PlanCatalogSettings,
131
+ typeof Readonly === "undefined" ? Object : Readonly
132
+ ])
133
+ ], PaymentGatewayRegistry);
134
+
135
+ // src/payments/payment-callback.service.ts
136
+ import { BadRequestException, Inject as Inject2, Injectable as Injectable2, Logger, NotFoundException } from "@nestjs/common";
137
+ import { PAYMENT_ERROR_CODES, isForeignPaymentMethodReferenceError, isPaymentCallbackRejectedError } from "@saasicat/core";
138
+ function _ts_decorate2(decorators, target, key, desc) {
139
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
140
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
141
+ 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;
142
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
143
+ }
144
+ __name(_ts_decorate2, "_ts_decorate");
145
+ function _ts_metadata2(k, v) {
146
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
147
+ }
148
+ __name(_ts_metadata2, "_ts_metadata");
149
+ function _ts_param2(paramIndex, decorator) {
150
+ return function(target, key) {
151
+ decorator(target, key, paramIndex);
152
+ };
153
+ }
154
+ __name(_ts_param2, "_ts_param");
155
+ var PaymentCallbackService = class _PaymentCallbackService {
156
+ static {
157
+ __name(this, "PaymentCallbackService");
158
+ }
159
+ registry;
160
+ log;
161
+ transactions;
162
+ logger = new Logger(_PaymentCallbackService.name);
163
+ handlers = /* @__PURE__ */ new Map();
164
+ constructor(registry, log, transactions) {
165
+ this.registry = registry;
166
+ this.log = log;
167
+ this.transactions = transactions;
168
+ }
169
+ /**
170
+ * Names who handles the setups of one kind of subject. Each kind has one
171
+ * handler; a second is a wiring error, not a replacement.
172
+ */
173
+ handleSetupsOf(kind, handler) {
174
+ if (this.handlers.has(kind)) {
175
+ throw new Error(`A handler for payment method setups of '${kind}' is registered twice.`);
176
+ }
177
+ this.handlers.set(kind, handler);
178
+ }
179
+ async handle(account, callback) {
180
+ const entry = this.registry.account(account);
181
+ if (!entry) {
182
+ throw new NotFoundException(codedError(PAYMENT_ERROR_CODES.PAYMENT_GATEWAY_ACCOUNT_UNKNOWN, {
183
+ account
184
+ }));
185
+ }
186
+ const event = await this.read(entry.gateway.readCallback.bind(entry.gateway), callback, account);
187
+ if (event.kind === "unhandled") {
188
+ this.logger.debug(`Payment event ${event.eventId} (${event.type}) at '${account}' needs no action.`);
189
+ return "unhandled";
190
+ }
191
+ const handler = this.handlerFor(event);
192
+ const afterCommit = [];
193
+ const claimed = await this.transactions.run(async (tx) => {
194
+ const fresh = await this.log.claim({
195
+ gatewayAccount: account,
196
+ eventId: event.eventId,
197
+ provider: entry.provider,
198
+ sessionId: event.sessionRef,
199
+ kind: event.kind,
200
+ summary: summaryOf(event)
201
+ }, tx);
202
+ if (!fresh) return false;
203
+ const context = {
204
+ tx,
205
+ gatewayAccount: account,
206
+ provider: entry.provider,
207
+ afterCommit: /* @__PURE__ */ __name((step) => afterCommit.push(step), "afterCommit")
208
+ };
209
+ if (event.kind === "payment-method-confirmed") {
210
+ const effect = await this.confirm(handler, event, context);
211
+ if (effect === "nothing-to-do") {
212
+ await this.log.releaseSession(account, event.eventId, tx);
213
+ }
214
+ } else {
215
+ await handler.failed(event, context);
216
+ }
217
+ return true;
218
+ });
219
+ if (!claimed) {
220
+ this.logger.log(`Payment event ${event.eventId} at '${account}' was handled before; duplicate ignored.`);
221
+ return "duplicate";
222
+ }
223
+ for (const step of afterCommit) {
224
+ try {
225
+ await step();
226
+ } catch (error) {
227
+ const reason = error instanceof Error ? error.message : String(error);
228
+ this.logger.error(`Payment event ${event.eventId} at '${account}' is handled, and a step after it failed: ${reason}`);
229
+ }
230
+ }
231
+ return "handled";
232
+ }
233
+ /**
234
+ * Hands the confirmation to its handler, and names the one refusal that
235
+ * will never come out differently.
236
+ *
237
+ * A reference belongs to one subscriber for good (`SC-SEC-014`), so a
238
+ * confirmation refused for that reason is refused on every delivery. It
239
+ * still leaves by throwing, which rolls this transaction back: by the time
240
+ * it is raised a handler has written something that must not stand on its
241
+ * own — the setup marked complete on the tenant's path, the whole
242
+ * activation on the sign-up's. Answering the gateway anything else would
243
+ * commit one of those without the payment method that justifies it.
244
+ *
245
+ * The gateway therefore retries until it gives up, and the claim rolls back
246
+ * with everything else, so nothing durable is left to say why. This line is
247
+ * what says it.
248
+ *
249
+ * There is no terminal state for it, and that is worth knowing before an
250
+ * operator waits for one: the webhook route has no filter for this, so every
251
+ * redelivery is answered `500`, for as long as the provider keeps trying. A
252
+ * provider that disables an endpoint after sustained failures would take the
253
+ * account's other callbacks with it. Giving the event an end would mean
254
+ * keeping the claim while undoing what the handler wrote, which needs a
255
+ * savepoint the port does not have — so it is named here rather than
256
+ * implied. It names the account, the reference and the subject the
257
+ * event was about — never the subscriber the reference belongs to, which is
258
+ * on the other side of the boundary being refused.
259
+ */
260
+ async confirm(handler, event, context) {
261
+ try {
262
+ return await handler.confirmed(event, context);
263
+ } catch (error) {
264
+ if (!isForeignPaymentMethodReferenceError(error)) throw error;
265
+ this.logger.error(`Payment event ${event.eventId} at '${context.gatewayAccount}' confirms payment method '${error.paymentMethodRef}' for ${subjectOf(event)}, and that reference belongs to another subscriber; nothing was recorded, and every delivery of this event is refused the same way.`);
266
+ throw error;
267
+ }
268
+ }
269
+ async read(readCallback, callback, account) {
270
+ try {
271
+ return await readCallback(callback);
272
+ } catch (error) {
273
+ if (!isPaymentCallbackRejectedError(error)) throw error;
274
+ this.logger.warn(`A payment callback for '${account}' was rejected: ${error.message}`);
275
+ throw new BadRequestException(codedError(PAYMENT_ERROR_CODES.PAYMENT_CALLBACK_REJECTED));
276
+ }
277
+ }
278
+ handlerFor(event) {
279
+ const handler = this.handlers.get(event.subject.kind);
280
+ if (handler) return handler;
281
+ throw new Error(`A payment method setup for a ${event.subject.kind} was reported, and nothing handles ` + (event.subject.kind === "registration" ? "sign-ups: wire RegistrationModule." : "subscriber payment methods."));
282
+ }
283
+ };
284
+ PaymentCallbackService = _ts_decorate2([
285
+ Injectable2(),
286
+ _ts_param2(1, Inject2(PAYMENT_EVENT_LOG_TOKEN)),
287
+ _ts_param2(2, Inject2(PAYMENT_TRANSACTION_RUNNER_TOKEN)),
288
+ _ts_metadata2("design:type", Function),
289
+ _ts_metadata2("design:paramtypes", [
290
+ typeof PaymentGatewayRegistry === "undefined" ? Object : PaymentGatewayRegistry,
291
+ typeof PaymentEventLog === "undefined" ? Object : PaymentEventLog,
292
+ typeof TransactionRunner === "undefined" ? Object : TransactionRunner
293
+ ])
294
+ ], PaymentCallbackService);
295
+ function subjectOf(event) {
296
+ return event.subject.kind === "registration" ? `sign-up ${event.subject.pendingRegistrationId}` : `subscriber ${event.subject.subscriberId}`;
297
+ }
298
+ __name(subjectOf, "subjectOf");
299
+ function summaryOf(event) {
300
+ const subject = event.subject.kind === "registration" ? {
301
+ registration: event.subject.pendingRegistrationId
302
+ } : {
303
+ subscriber: event.subject.subscriberId
304
+ };
305
+ if (event.kind === "payment-method-setup-failed") return {
306
+ ...subject
307
+ };
308
+ const { type, brand, last4 } = event.paymentMethod;
309
+ return {
310
+ ...subject,
311
+ type,
312
+ brand,
313
+ last4
314
+ };
315
+ }
316
+ __name(summaryOf, "summaryOf");
317
+
318
+ // src/payments/return-urls.ts
319
+ import { BadRequestException as BadRequestException2 } from "@nestjs/common";
320
+ import { PAYMENT_ERROR_CODES as PAYMENT_ERROR_CODES2 } from "@saasicat/core";
321
+ function refuseForeignReturnUrls(urls, allowedOrigins) {
322
+ const allowed = new Set(allowedOrigins.map(originOf));
323
+ for (const field of [
324
+ "successUrl",
325
+ "cancelUrl"
326
+ ]) {
327
+ const origin = originOf(urls[field]);
328
+ if (origin === null || !allowed.has(origin)) {
329
+ throw new BadRequestException2(codedError(PAYMENT_ERROR_CODES2.PAYMENT_RETURN_URL_NOT_ALLOWED, {
330
+ field
331
+ }));
332
+ }
333
+ }
334
+ }
335
+ __name(refuseForeignReturnUrls, "refuseForeignReturnUrls");
336
+ function originOf(url) {
337
+ try {
338
+ return new URL(url).origin;
339
+ } catch {
340
+ return null;
341
+ }
342
+ }
343
+ __name(originOf, "originOf");
344
+
345
+ export {
346
+ PAYMENT_GATEWAYS_TOKEN,
347
+ PAYMENT_EVENT_LOG_TOKEN,
348
+ SUBSCRIBER_PAYMENT_METHOD_REPOSITORY_TOKEN,
349
+ PAYMENT_TRANSACTION_RUNNER_TOKEN,
350
+ PaymentGatewayRegistry,
351
+ PaymentCallbackService,
352
+ refuseForeignReturnUrls
353
+ };