@saasicat/nest 0.18.1 → 0.21.0
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.
- package/dist/_entries.cjs +1341 -590
- package/dist/admin/index.js +2 -1
- package/dist/billing/index.d.cts +10 -6
- package/dist/billing/index.d.ts +10 -6
- package/dist/billing/index.js +7 -6
- package/dist/catalog/index.js +4 -4
- package/dist/checkout-offer/index.js +2 -2
- package/dist/{chunk-RZVSKKXX.js → chunk-3DRZ5GCP.js} +56 -105
- package/dist/{chunk-GLLCB4BP.js → chunk-4O6R6ARQ.js} +23 -13
- package/dist/{chunk-7RRWI5LH.js → chunk-55QGAZRQ.js} +10 -10
- package/dist/{chunk-Z3FWN3HJ.js → chunk-5ZXBXYXU.js} +3 -3
- package/dist/{chunk-V34AA7C7.js → chunk-6WL6VQ7Z.js} +1 -1
- package/dist/{chunk-ABB2CRAS.js → chunk-74SCJ4HF.js} +1 -1
- package/dist/{chunk-EXOLOJFZ.js → chunk-AGOG5B77.js} +36 -9
- package/dist/{chunk-32EMXPU2.js → chunk-B7NRY2LV.js} +48 -17
- package/dist/{chunk-MDIZUVIK.js → chunk-BSK6YBLI.js} +1 -1
- package/dist/{chunk-R2QOSVAA.js → chunk-DQKCK7DX.js} +1 -1
- package/dist/chunk-EBJJNTW6.js +22 -0
- package/dist/{chunk-PLJEAUFU.js → chunk-FYWLPYBK.js} +79 -39
- package/dist/{chunk-P3DP6GJE.js → chunk-GC5AKN44.js} +487 -160
- package/dist/{chunk-5KH2O4SF.js → chunk-NZLAYCEX.js} +286 -110
- package/dist/{chunk-E7AJ42AB.js → chunk-VQQWF7MW.js} +172 -28
- package/dist/{chunk-GJBXOCRZ.js → chunk-XTVAFJ3P.js} +46 -17
- package/dist/{chunk-BOADBIGB.js → chunk-YYUXKLXK.js} +2 -2
- package/dist/{chunk-YITTE646.js → chunk-ZH5PYEMI.js} +41 -23
- package/dist/{define-saasicat-Rc0rXrYG.d.ts → define-saasicat-B20Ms8UC.d.ts} +1 -1
- package/dist/{define-saasicat-CeDBX9ji.d.cts → define-saasicat-Cf9DpEsY.d.cts} +1 -1
- package/dist/discovery/index.d.cts +2 -2
- package/dist/discovery/index.d.ts +2 -2
- package/dist/discovery/index.js +4 -4
- package/dist/{enforce-quota.interceptor-CmbWX39L.d.ts → enforce-quota.interceptor-CUyfR4cQ.d.ts} +1 -1
- package/dist/{enforce-quota.interceptor-DMw-bPyk.d.cts → enforce-quota.interceptor-Dtg5WdFj.d.cts} +1 -1
- package/dist/entitlement/index.js +3 -2
- package/dist/index.d.cts +27 -8
- package/dist/index.d.ts +27 -8
- package/dist/index.js +21 -17
- package/dist/{module-DgYJur6f.d.ts → module-BE17XFLg.d.ts} +1 -1
- package/dist/{module-CSN8j8Tp.d.cts → module-CHMIyWlf.d.cts} +1 -1
- package/dist/platform/index.d.cts +8 -8
- package/dist/platform/index.d.ts +8 -8
- package/dist/platform/index.js +14 -13
- package/dist/promo/index.d.cts +3 -3
- package/dist/promo/index.d.ts +3 -3
- package/dist/promo/index.js +4 -3
- package/dist/registration/index.js +2 -1
- package/dist/{saas-platform.module-D3rUTiho.d.cts → saas-platform.module-BfaYfL-G.d.cts} +1 -1
- package/dist/{saas-platform.module-DLhoqKNA.d.ts → saas-platform.module-DChhDrR-.d.ts} +1 -1
- package/dist/{service-DX8KbGXl.d.cts → service-D48clObR.d.cts} +3 -3
- package/dist/{service-DX8KbGXl.d.ts → service-D48clObR.d.ts} +3 -3
- package/dist/subscription-contract/index.js +2 -2
- package/dist/{tenant-billing.controller-CG4CaFUP.d.cts → tenant-billing.controller-BIF-mstX.d.cts} +2 -2
- package/dist/{tenant-billing.controller-BwZRnJ3q.d.ts → tenant-billing.controller-DT5UrnDX.d.ts} +2 -2
- package/dist/testing/index.d.cts +5 -5
- package/dist/testing/index.d.ts +5 -5
- package/dist/testing/index.js +34 -33
- package/package.json +3 -3
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
computeIncludedVat,
|
|
4
4
|
getPlanPriceGross,
|
|
5
5
|
round2
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-DQKCK7DX.js";
|
|
7
7
|
import {
|
|
8
8
|
__name
|
|
9
9
|
} from "./chunk-SHUYVCID.js";
|
|
@@ -50,15 +50,15 @@ function buildLabel(code, cycle) {
|
|
|
50
50
|
maximumFractionDigits: 2
|
|
51
51
|
})} \u20AC`;
|
|
52
52
|
if (code.durationType === "ONCE") {
|
|
53
|
-
return `${valueStr}
|
|
53
|
+
return `${valueStr} once`;
|
|
54
54
|
}
|
|
55
55
|
if (code.durationType === "MONTHS") {
|
|
56
56
|
const m = code.durationValue ?? 0;
|
|
57
|
-
return m === 1 ? `${valueStr}
|
|
57
|
+
return m === 1 ? `${valueStr} for the first month` : `${valueStr} for ${m} months`;
|
|
58
58
|
}
|
|
59
59
|
const n = code.durationValue ?? 0;
|
|
60
|
-
if (n === 1) return cycle === "YEARLY" ? `${valueStr}
|
|
61
|
-
return cycle === "YEARLY" ? `${valueStr}
|
|
60
|
+
if (n === 1) return cycle === "YEARLY" ? `${valueStr} for the first year` : `${valueStr} for the first month`;
|
|
61
|
+
return cycle === "YEARLY" ? `${valueStr} for the first ${n} years` : `${valueStr} for the first ${n} months`;
|
|
62
62
|
}
|
|
63
63
|
__name(buildLabel, "buildLabel");
|
|
64
64
|
|
|
@@ -74,6 +74,7 @@ var PROMO_SERVICE_CONFIG_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/Promo
|
|
|
74
74
|
|
|
75
75
|
// src/promo/service.ts
|
|
76
76
|
import { BadRequestException, Inject, Injectable, NotFoundException } from "@nestjs/common";
|
|
77
|
+
import { BILLING_ERROR_CODES, PROMO_ERROR_CODES } from "@saasicat/types";
|
|
77
78
|
function _ts_decorate(decorators, target, key, desc) {
|
|
78
79
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
79
80
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -91,7 +92,23 @@ function _ts_param(paramIndex, decorator) {
|
|
|
91
92
|
};
|
|
92
93
|
}
|
|
93
94
|
__name(_ts_param, "_ts_param");
|
|
94
|
-
var
|
|
95
|
+
var CODE_MIN_LENGTH = 4;
|
|
96
|
+
var CODE_MAX_LENGTH = 32;
|
|
97
|
+
var CODE_PATTERN = new RegExp(`^[A-Z0-9_-]{${CODE_MIN_LENGTH},${CODE_MAX_LENGTH}}$`);
|
|
98
|
+
var PERCENT_MIN_EXCLUSIVE = 0;
|
|
99
|
+
var PERCENT_MAX = 100;
|
|
100
|
+
var DURATION_MIN = 1;
|
|
101
|
+
var DURATION_MAX = 24;
|
|
102
|
+
function notRedeemable(reason) {
|
|
103
|
+
return new BadRequestException({
|
|
104
|
+
code: PROMO_ERROR_CODES.PROMO_CODE_NOT_REDEEMABLE,
|
|
105
|
+
message: `Code cannot be redeemed: ${reason}`,
|
|
106
|
+
params: {
|
|
107
|
+
reason
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
__name(notRedeemable, "notRedeemable");
|
|
95
112
|
var PromoCodesService = class {
|
|
96
113
|
static {
|
|
97
114
|
__name(this, "PromoCodesService");
|
|
@@ -120,46 +137,118 @@ var PromoCodesService = class {
|
|
|
120
137
|
async create(input) {
|
|
121
138
|
const code = input.code.trim().toUpperCase();
|
|
122
139
|
if (!CODE_PATTERN.test(code)) {
|
|
123
|
-
throw new BadRequestException(
|
|
140
|
+
throw new BadRequestException({
|
|
141
|
+
code: PROMO_ERROR_CODES.PROMO_CODE_FORMAT_INVALID,
|
|
142
|
+
message: 'The code may only contain upper-case letters, digits, "-" and "_" (4\u201332 characters).',
|
|
143
|
+
params: {
|
|
144
|
+
promoCode: code,
|
|
145
|
+
minLength: CODE_MIN_LENGTH,
|
|
146
|
+
maxLength: CODE_MAX_LENGTH
|
|
147
|
+
}
|
|
148
|
+
});
|
|
124
149
|
}
|
|
125
150
|
if (input.valueType === "PERCENT") {
|
|
126
|
-
if (!(input.value >
|
|
127
|
-
throw new BadRequestException(
|
|
151
|
+
if (!(input.value > PERCENT_MIN_EXCLUSIVE && input.value <= PERCENT_MAX)) {
|
|
152
|
+
throw new BadRequestException({
|
|
153
|
+
code: PROMO_ERROR_CODES.PROMO_PERCENT_OUT_OF_RANGE,
|
|
154
|
+
message: "The percentage must be between 0 and 100.",
|
|
155
|
+
params: {
|
|
156
|
+
value: input.value,
|
|
157
|
+
min: PERCENT_MIN_EXCLUSIVE,
|
|
158
|
+
max: PERCENT_MAX
|
|
159
|
+
}
|
|
160
|
+
});
|
|
128
161
|
}
|
|
129
162
|
} else if (!(input.value > 0)) {
|
|
130
|
-
throw new BadRequestException(
|
|
163
|
+
throw new BadRequestException({
|
|
164
|
+
code: PROMO_ERROR_CODES.PROMO_AMOUNT_NOT_POSITIVE,
|
|
165
|
+
message: "The amount must be positive.",
|
|
166
|
+
params: {
|
|
167
|
+
value: input.value,
|
|
168
|
+
min: 0
|
|
169
|
+
}
|
|
170
|
+
});
|
|
131
171
|
}
|
|
132
172
|
if (input.durationType === "ONCE") {
|
|
133
173
|
if (input.durationValue != null) {
|
|
134
|
-
throw new BadRequestException(
|
|
174
|
+
throw new BadRequestException({
|
|
175
|
+
code: PROMO_ERROR_CODES.PROMO_ONE_OFF_WITH_DURATION,
|
|
176
|
+
message: "A one-off discount must not set a duration.",
|
|
177
|
+
params: {
|
|
178
|
+
durationValue: input.durationValue
|
|
179
|
+
}
|
|
180
|
+
});
|
|
135
181
|
}
|
|
136
182
|
} else {
|
|
137
183
|
const v = input.durationValue;
|
|
138
|
-
if (v == null || v <
|
|
139
|
-
throw new BadRequestException(
|
|
184
|
+
if (v == null || v < DURATION_MIN || v > DURATION_MAX) {
|
|
185
|
+
throw new BadRequestException({
|
|
186
|
+
code: PROMO_ERROR_CODES.PROMO_DURATION_INVALID,
|
|
187
|
+
message: "Invalid duration (at most 24 months or billing periods).",
|
|
188
|
+
params: {
|
|
189
|
+
durationValue: v ?? null,
|
|
190
|
+
min: DURATION_MIN,
|
|
191
|
+
max: DURATION_MAX
|
|
192
|
+
}
|
|
193
|
+
});
|
|
140
194
|
}
|
|
141
195
|
}
|
|
142
196
|
if (input.validFrom && input.validUntil && input.validFrom >= input.validUntil) {
|
|
143
|
-
throw new BadRequestException(
|
|
197
|
+
throw new BadRequestException({
|
|
198
|
+
code: PROMO_ERROR_CODES.PROMO_VALIDITY_WINDOW_INVALID,
|
|
199
|
+
message: "Invalid validity window.",
|
|
200
|
+
params: {
|
|
201
|
+
validFrom: input.validFrom.toISOString(),
|
|
202
|
+
validUntil: input.validUntil.toISOString()
|
|
203
|
+
}
|
|
204
|
+
});
|
|
144
205
|
}
|
|
145
206
|
const plans = input.appliesToPlans ?? [];
|
|
146
207
|
const blocked = this.config.nonRedeemablePlans ?? [];
|
|
147
208
|
for (const p of plans) {
|
|
148
209
|
if (blocked.includes(p)) {
|
|
149
|
-
throw new BadRequestException(
|
|
210
|
+
throw new BadRequestException({
|
|
211
|
+
code: PROMO_ERROR_CODES.PROMO_PLAN_NOT_DISCOUNTABLE,
|
|
212
|
+
message: `${p}-Plan cannot be discounted.`,
|
|
213
|
+
params: {
|
|
214
|
+
plan: p
|
|
215
|
+
}
|
|
216
|
+
});
|
|
150
217
|
}
|
|
151
218
|
}
|
|
152
219
|
if (input.minimumPlanAmountGross != null && input.minimumPlanAmountGross <= 0) {
|
|
153
|
-
throw new BadRequestException(
|
|
220
|
+
throw new BadRequestException({
|
|
221
|
+
code: PROMO_ERROR_CODES.PROMO_MIN_AMOUNT_NOT_POSITIVE,
|
|
222
|
+
message: "The minimum plan gross amount must be positive.",
|
|
223
|
+
params: {
|
|
224
|
+
value: input.minimumPlanAmountGross,
|
|
225
|
+
min: 0
|
|
226
|
+
}
|
|
227
|
+
});
|
|
154
228
|
}
|
|
155
229
|
if (input.valueType === "ABSOLUTE" && !input.allowZeroInvoice) {
|
|
156
230
|
const lowestApplicableGross = this.lowestApplicablePlanGross(plans);
|
|
157
231
|
if (lowestApplicableGross != null && input.value >= lowestApplicableGross) {
|
|
158
|
-
throw new BadRequestException(
|
|
232
|
+
throw new BadRequestException({
|
|
233
|
+
code: PROMO_ERROR_CODES.PROMO_WOULD_PRODUCE_ZERO_INVOICE,
|
|
234
|
+
message: "For absolute amounts the discount must stay below the lowest applicable plan price, or allowZeroInvoice must be enabled.",
|
|
235
|
+
params: {
|
|
236
|
+
value: input.value,
|
|
237
|
+
lowestApplicablePlanGross: lowestApplicableGross
|
|
238
|
+
}
|
|
239
|
+
});
|
|
159
240
|
}
|
|
160
241
|
}
|
|
161
242
|
const exists = await this.promoRepo.findByCode(code);
|
|
162
|
-
if (exists)
|
|
243
|
+
if (exists) {
|
|
244
|
+
throw new BadRequestException({
|
|
245
|
+
code: PROMO_ERROR_CODES.PROMO_CODE_ALREADY_EXISTS,
|
|
246
|
+
message: "The code already exists.",
|
|
247
|
+
params: {
|
|
248
|
+
promoCode: code
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
}
|
|
163
252
|
return this.promoRepo.create({
|
|
164
253
|
...input,
|
|
165
254
|
code
|
|
@@ -167,10 +256,26 @@ var PromoCodesService = class {
|
|
|
167
256
|
}
|
|
168
257
|
async update(id, input) {
|
|
169
258
|
const existing = await this.promoRepo.findById(id);
|
|
170
|
-
if (!existing)
|
|
259
|
+
if (!existing) {
|
|
260
|
+
throw new NotFoundException({
|
|
261
|
+
code: PROMO_ERROR_CODES.PROMO_CODE_NOT_FOUND,
|
|
262
|
+
message: "Code not found",
|
|
263
|
+
params: {
|
|
264
|
+
promoCodeId: id,
|
|
265
|
+
promoCode: null
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
}
|
|
171
269
|
if (input.maxRedemptions != null && existing.maxRedemptions != null) {
|
|
172
270
|
if (input.maxRedemptions < existing.maxRedemptions) {
|
|
173
|
-
throw new BadRequestException(
|
|
271
|
+
throw new BadRequestException({
|
|
272
|
+
code: PROMO_ERROR_CODES.PROMO_MAX_REDEMPTIONS_LOWERED,
|
|
273
|
+
message: "maxRedemptions cannot be lowered.",
|
|
274
|
+
params: {
|
|
275
|
+
current: existing.maxRedemptions,
|
|
276
|
+
requested: input.maxRedemptions
|
|
277
|
+
}
|
|
278
|
+
});
|
|
174
279
|
}
|
|
175
280
|
}
|
|
176
281
|
return this.promoRepo.update(id, input);
|
|
@@ -178,7 +283,14 @@ var PromoCodesService = class {
|
|
|
178
283
|
async softDelete(id) {
|
|
179
284
|
const redemptions = await this.redemptionRepo.countByPromoCode(id);
|
|
180
285
|
if (redemptions > 0) {
|
|
181
|
-
throw new BadRequestException(
|
|
286
|
+
throw new BadRequestException({
|
|
287
|
+
code: PROMO_ERROR_CODES.PROMO_CODE_HAS_REDEMPTIONS,
|
|
288
|
+
message: "The code already has redemptions \u2014 it cannot be soft-deleted. Pause it instead.",
|
|
289
|
+
params: {
|
|
290
|
+
promoCodeId: id,
|
|
291
|
+
redemptions
|
|
292
|
+
}
|
|
293
|
+
});
|
|
182
294
|
}
|
|
183
295
|
await this.promoRepo.softDelete(id);
|
|
184
296
|
}
|
|
@@ -189,7 +301,16 @@ var PromoCodesService = class {
|
|
|
189
301
|
async findOne(id) {
|
|
190
302
|
await this.lazyExpire();
|
|
191
303
|
const code = await this.promoRepo.findById(id);
|
|
192
|
-
if (!code || code.deletedAt)
|
|
304
|
+
if (!code || code.deletedAt) {
|
|
305
|
+
throw new NotFoundException({
|
|
306
|
+
code: PROMO_ERROR_CODES.PROMO_CODE_NOT_FOUND,
|
|
307
|
+
message: "Code not found",
|
|
308
|
+
params: {
|
|
309
|
+
promoCodeId: id,
|
|
310
|
+
promoCode: null
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
}
|
|
193
314
|
return code;
|
|
194
315
|
}
|
|
195
316
|
async stats(id) {
|
|
@@ -317,28 +438,51 @@ var PromoCodesService = class {
|
|
|
317
438
|
const code = input.code.trim().toUpperCase();
|
|
318
439
|
const promo = await this.promoRepo.findByCode(code, tx);
|
|
319
440
|
if (!promo || promo.deletedAt) {
|
|
320
|
-
throw new BadRequestException(
|
|
441
|
+
throw new BadRequestException({
|
|
442
|
+
code: PROMO_ERROR_CODES.PROMO_CODE_NOT_FOUND,
|
|
443
|
+
// The lookup runs on the code string, so there is no id to report.
|
|
444
|
+
message: "Code not found",
|
|
445
|
+
params: {
|
|
446
|
+
promoCodeId: null,
|
|
447
|
+
promoCode: code
|
|
448
|
+
}
|
|
449
|
+
});
|
|
321
450
|
}
|
|
322
451
|
const sub = await this.subscriptionLookup.findById(input.subscriptionId, tx);
|
|
323
|
-
if (!sub)
|
|
452
|
+
if (!sub) {
|
|
453
|
+
throw new NotFoundException({
|
|
454
|
+
code: BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
|
|
455
|
+
message: "Subscription not found",
|
|
456
|
+
params: {
|
|
457
|
+
tenantId: input.tenantId
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
}
|
|
324
461
|
if (sub.tenantId !== input.tenantId) {
|
|
325
|
-
throw new BadRequestException(
|
|
462
|
+
throw new BadRequestException({
|
|
463
|
+
code: BILLING_ERROR_CODES.SUBSCRIPTION_TENANT_MISMATCH,
|
|
464
|
+
message: "Subscription does not belong to the tenant",
|
|
465
|
+
params: {
|
|
466
|
+
subscriptionId: sub.id,
|
|
467
|
+
tenantId: input.tenantId
|
|
468
|
+
}
|
|
469
|
+
});
|
|
326
470
|
}
|
|
327
471
|
const reason = this.checkEligibility(promo, {
|
|
328
472
|
code,
|
|
329
473
|
planId: sub.plan,
|
|
330
474
|
billingCycle: sub.billingCycle
|
|
331
475
|
});
|
|
332
|
-
if (reason) throw
|
|
476
|
+
if (reason) throw notRedeemable(reason);
|
|
333
477
|
const firstTimeReason = await this.checkFirstTimeCustomer(promo, input.email, {
|
|
334
478
|
requireEmail: true
|
|
335
479
|
});
|
|
336
480
|
if (firstTimeReason) {
|
|
337
|
-
throw
|
|
481
|
+
throw notRedeemable(firstTimeReason);
|
|
338
482
|
}
|
|
339
483
|
const claimed = await this.promoRepo.claimSlot(promo.id, tx);
|
|
340
484
|
if (!claimed) {
|
|
341
|
-
throw
|
|
485
|
+
throw notRedeemable("EXHAUSTED");
|
|
342
486
|
}
|
|
343
487
|
await this.promoRepo.markExhaustedIfFull(promo.id, tx);
|
|
344
488
|
const startsAt = sub.startedAt ?? /* @__PURE__ */ new Date();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
appendImplicitDiscountLineItem
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-BSK6YBLI.js";
|
|
4
4
|
import {
|
|
5
5
|
SaaSiCatPublicRoute
|
|
6
6
|
} from "./chunk-YSC46BGM.js";
|
|
@@ -21,7 +21,7 @@ var CHECKOUT_OFFER_REPOSITORY_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/
|
|
|
21
21
|
|
|
22
22
|
// src/checkout-offer/checkout-offer.service.ts
|
|
23
23
|
import { ConflictException, Inject, Injectable, NotFoundException, Optional, UnprocessableEntityException } from "@nestjs/common";
|
|
24
|
-
import { buildFeatureRequiresIndex, collectUnsatisfiedRequires, startOfUtcDay } from "@saasicat/types";
|
|
24
|
+
import { CONTRACT_ERROR_CODES, buildFeatureRequiresIndex, collectUnsatisfiedRequires, startOfUtcDay } from "@saasicat/types";
|
|
25
25
|
function _ts_decorate(decorators, target, key, desc) {
|
|
26
26
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
27
27
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -59,7 +59,13 @@ var CheckoutOfferService = class {
|
|
|
59
59
|
async getById(id) {
|
|
60
60
|
const row = await this.repo.findById(id);
|
|
61
61
|
if (!row) {
|
|
62
|
-
throw new NotFoundException(
|
|
62
|
+
throw new NotFoundException({
|
|
63
|
+
code: CONTRACT_ERROR_CODES.CHECKOUT_OFFER_NOT_FOUND,
|
|
64
|
+
message: `CheckoutOffer '${id}' not found`,
|
|
65
|
+
params: {
|
|
66
|
+
offerId: id
|
|
67
|
+
}
|
|
68
|
+
});
|
|
63
69
|
}
|
|
64
70
|
return row;
|
|
65
71
|
}
|
|
@@ -77,7 +83,7 @@ var CheckoutOfferService = class {
|
|
|
77
83
|
/** Customization in onboarding — only while the offer is `open`. */
|
|
78
84
|
async update(id, data) {
|
|
79
85
|
const existing = await this.getById(id);
|
|
80
|
-
this.assertOpen(existing, "
|
|
86
|
+
this.assertOpen(existing, "changed");
|
|
81
87
|
const next = this.normalizeUpdateData(existing, data);
|
|
82
88
|
await this.assertFeatureRequiresSatisfied({
|
|
83
89
|
projectKey: existing.projectKey,
|
|
@@ -95,7 +101,7 @@ var CheckoutOfferService = class {
|
|
|
95
101
|
*/
|
|
96
102
|
async consume(id) {
|
|
97
103
|
const existing = await this.getById(id);
|
|
98
|
-
this.assertOpen(existing, "
|
|
104
|
+
this.assertOpen(existing, "consumed");
|
|
99
105
|
await this.assertBundleVersionsStillBookable(existing);
|
|
100
106
|
return this.repo.consume(id);
|
|
101
107
|
}
|
|
@@ -124,8 +130,8 @@ var CheckoutOfferService = class {
|
|
|
124
130
|
], requiresIndex);
|
|
125
131
|
if (missingRequires.length > 0) {
|
|
126
132
|
throw new UnprocessableEntityException({
|
|
127
|
-
code:
|
|
128
|
-
message: `
|
|
133
|
+
code: CONTRACT_ERROR_CODES.CHECKOUT_OFFER_FEATURE_DEPENDENCY_UNSATISFIED,
|
|
134
|
+
message: `The selected plan does not cover all feature dependencies: [${missingRequires.join(", ")}] are missing from the plan + selected bundles.`,
|
|
129
135
|
missingRequires
|
|
130
136
|
});
|
|
131
137
|
}
|
|
@@ -205,8 +211,8 @@ var CheckoutOfferService = class {
|
|
|
205
211
|
}
|
|
206
212
|
if (violations.length > 0) {
|
|
207
213
|
throw new UnprocessableEntityException({
|
|
208
|
-
code:
|
|
209
|
-
message: "
|
|
214
|
+
code: CONTRACT_ERROR_CODES.CHECKOUT_OFFER_BUNDLE_VERSION_NOT_BOOKABLE,
|
|
215
|
+
message: "At least one bundle version from the checkout offer is no longer bookable.",
|
|
210
216
|
violations
|
|
211
217
|
});
|
|
212
218
|
}
|
|
@@ -260,15 +266,15 @@ var CheckoutOfferService = class {
|
|
|
260
266
|
const hasPlan = lineItems.some((item) => item.kind === "plan");
|
|
261
267
|
if (!hasPlan) {
|
|
262
268
|
throw new UnprocessableEntityException({
|
|
263
|
-
code:
|
|
264
|
-
message: "
|
|
269
|
+
code: CONTRACT_ERROR_CODES.CHECKOUT_OFFER_PLAN_LINE_ITEM_REQUIRED,
|
|
270
|
+
message: "A checkout offer requires a frozen plan line item."
|
|
265
271
|
});
|
|
266
272
|
}
|
|
267
273
|
const missingBundleVersionIds = input.bundleVersionIds.filter((versionId) => !lineItems.some((item) => item.kind === "bundle" && item.sourceVersionId === versionId));
|
|
268
274
|
if (missingBundleVersionIds.length > 0) {
|
|
269
275
|
throw new UnprocessableEntityException({
|
|
270
|
-
code:
|
|
271
|
-
message: "
|
|
276
|
+
code: CONTRACT_ERROR_CODES.CHECKOUT_OFFER_BUNDLE_LINE_ITEMS_REQUIRED,
|
|
277
|
+
message: "Every selected bundle version requires a frozen bundle line item.",
|
|
272
278
|
bundleVersionIds: missingBundleVersionIds
|
|
273
279
|
});
|
|
274
280
|
}
|
|
@@ -300,13 +306,36 @@ var CheckoutOfferService = class {
|
|
|
300
306
|
}
|
|
301
307
|
assertOpen(existing, action) {
|
|
302
308
|
if (existing.status === "consumed") {
|
|
303
|
-
throw new ConflictException(
|
|
309
|
+
throw new ConflictException({
|
|
310
|
+
code: CONTRACT_ERROR_CODES.CHECKOUT_OFFER_ALREADY_CONSUMED,
|
|
311
|
+
message: `Checkout offer '${existing.id}' has already been consumed and cannot be ${action}`,
|
|
312
|
+
params: {
|
|
313
|
+
offerId: existing.id,
|
|
314
|
+
action
|
|
315
|
+
}
|
|
316
|
+
});
|
|
304
317
|
}
|
|
305
318
|
if (existing.status === "expired") {
|
|
306
|
-
throw new ConflictException(
|
|
319
|
+
throw new ConflictException({
|
|
320
|
+
code: CONTRACT_ERROR_CODES.CHECKOUT_OFFER_EXPIRED,
|
|
321
|
+
message: `Checkout offer '${existing.id}' has expired and cannot be ${action}`,
|
|
322
|
+
params: {
|
|
323
|
+
offerId: existing.id,
|
|
324
|
+
action,
|
|
325
|
+
validUntil: existing.validUntil ?? null
|
|
326
|
+
}
|
|
327
|
+
});
|
|
307
328
|
}
|
|
308
329
|
if (this.isExpired(existing)) {
|
|
309
|
-
throw new ConflictException(
|
|
330
|
+
throw new ConflictException({
|
|
331
|
+
code: CONTRACT_ERROR_CODES.CHECKOUT_OFFER_EXPIRED,
|
|
332
|
+
message: `CheckoutOffer '${existing.id}' has expired and cannot be ${action}`,
|
|
333
|
+
params: {
|
|
334
|
+
offerId: existing.id,
|
|
335
|
+
action,
|
|
336
|
+
validUntil: existing.validUntil ?? null
|
|
337
|
+
}
|
|
338
|
+
});
|
|
310
339
|
}
|
|
311
340
|
}
|
|
312
341
|
isExpired(row) {
|
|
@@ -385,7 +414,7 @@ var CreateCheckoutOfferDto = class {
|
|
|
385
414
|
_ts_decorate2([
|
|
386
415
|
IsString(),
|
|
387
416
|
Matches(PROJECT_KEY_PATTERN, {
|
|
388
|
-
message: "projectKey
|
|
417
|
+
message: "projectKey must be kebab-case"
|
|
389
418
|
}),
|
|
390
419
|
MaxLength(64),
|
|
391
420
|
_ts_metadata2("design:type", String)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DiscoveryScanner
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-5ZXBXYXU.js";
|
|
4
4
|
import {
|
|
5
5
|
DEFINES_QUOTA_KEY,
|
|
6
6
|
DISCOVERY_SNAPSHOT_TOKEN,
|
|
@@ -44,7 +44,7 @@ async function runHeadlessDiscoveryScan(appModule, options = {}) {
|
|
|
44
44
|
await app.close();
|
|
45
45
|
}
|
|
46
46
|
} catch (err) {
|
|
47
|
-
console.error("[discovery-scan]
|
|
47
|
+
console.error("[discovery-scan] ERROR:", err instanceof Error ? err.message : err);
|
|
48
48
|
return exit(4);
|
|
49
49
|
}
|
|
50
50
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
PromoCodesModule
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-4O6R6ARQ.js";
|
|
4
4
|
import {
|
|
5
5
|
AdminManifestModule,
|
|
6
6
|
AdminManifestService,
|
|
@@ -9,36 +9,39 @@ import {
|
|
|
9
9
|
AdminStatsModule,
|
|
10
10
|
MfaService,
|
|
11
11
|
SuperAdminGuard
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-FYWLPYBK.js";
|
|
13
13
|
import {
|
|
14
14
|
PublicCatalogModule,
|
|
15
15
|
REQUIRE_FEATURE_KEY,
|
|
16
16
|
SubscriptionBundleModule,
|
|
17
17
|
TenantBillingModule
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-NZLAYCEX.js";
|
|
19
19
|
import {
|
|
20
20
|
SubscriptionContractModule
|
|
21
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-B7NRY2LV.js";
|
|
22
22
|
import {
|
|
23
23
|
CatalogModule
|
|
24
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-GC5AKN44.js";
|
|
25
25
|
import {
|
|
26
26
|
CheckoutOfferModule
|
|
27
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-XTVAFJ3P.js";
|
|
28
28
|
import {
|
|
29
29
|
SaaSiCatPublicRoute
|
|
30
30
|
} from "./chunk-YSC46BGM.js";
|
|
31
31
|
import {
|
|
32
32
|
DiscoveryModule
|
|
33
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-6WL6VQ7Z.js";
|
|
34
34
|
import {
|
|
35
35
|
EntitlementModule,
|
|
36
36
|
LimitExceededError
|
|
37
|
-
} from "./chunk-
|
|
37
|
+
} from "./chunk-AGOG5B77.js";
|
|
38
38
|
import {
|
|
39
39
|
PLAN_CATALOG_TOKEN,
|
|
40
40
|
PlanCatalogModule
|
|
41
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-DQKCK7DX.js";
|
|
42
|
+
import {
|
|
43
|
+
codedError
|
|
44
|
+
} from "./chunk-EBJJNTW6.js";
|
|
42
45
|
import {
|
|
43
46
|
asProvider
|
|
44
47
|
} from "./chunk-SABTXESR.js";
|
|
@@ -105,9 +108,7 @@ var SetupService = class {
|
|
|
105
108
|
await this.assertNoSuperAdmin();
|
|
106
109
|
const email = req.email.trim().toLowerCase();
|
|
107
110
|
if (!email.includes("@")) {
|
|
108
|
-
throw new BadRequestException(
|
|
109
|
-
code: SETUP_ERROR_CODES.INVALID_EMAIL
|
|
110
|
-
});
|
|
111
|
+
throw new BadRequestException(codedError(SETUP_ERROR_CODES.INVALID_EMAIL));
|
|
111
112
|
}
|
|
112
113
|
const generated = !req.password;
|
|
113
114
|
const password = req.password ?? randomBytes(GENERATED_PASSWORD_BYTES).toString("base64url");
|
|
@@ -157,20 +158,19 @@ var SetupService = class {
|
|
|
157
158
|
if (!expected) {
|
|
158
159
|
throw new ForbiddenException({
|
|
159
160
|
code: SETUP_ERROR_CODES.SETUP_DISABLED,
|
|
160
|
-
message: `${this.config.setupTokenEnvVar}
|
|
161
|
+
message: `${this.config.setupTokenEnvVar} is not set \u2014 setup is disabled.`,
|
|
162
|
+
params: {
|
|
163
|
+
envVar: this.config.setupTokenEnvVar
|
|
164
|
+
}
|
|
161
165
|
});
|
|
162
166
|
}
|
|
163
167
|
if (!token || !timingSafeEqualStr(token, expected)) {
|
|
164
|
-
throw new UnauthorizedException(
|
|
165
|
-
code: SETUP_ERROR_CODES.INVALID_SETUP_TOKEN
|
|
166
|
-
});
|
|
168
|
+
throw new UnauthorizedException(codedError(SETUP_ERROR_CODES.INVALID_SETUP_TOKEN));
|
|
167
169
|
}
|
|
168
170
|
}
|
|
169
171
|
async assertNoSuperAdmin() {
|
|
170
172
|
if (await this.users.countSuperAdmins() > 0) {
|
|
171
|
-
throw new ConflictException(
|
|
172
|
-
code: SETUP_ERROR_CODES.SETUP_ALREADY_DONE
|
|
173
|
-
});
|
|
173
|
+
throw new ConflictException(codedError(SETUP_ERROR_CODES.SETUP_ALREADY_DONE));
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
176
|
};
|
|
@@ -448,6 +448,7 @@ StaticEntitlementService = _ts_decorate4([
|
|
|
448
448
|
// src/platform/static-feature.guard.ts
|
|
449
449
|
import { ForbiddenException as ForbiddenException2, Inject as Inject3, Injectable as Injectable3, Optional } from "@nestjs/common";
|
|
450
450
|
import { Reflector } from "@nestjs/core";
|
|
451
|
+
import { AUTH_ERROR_CODES, FEATURE_NOT_LICENSED } from "@saasicat/types";
|
|
451
452
|
function _ts_decorate5(decorators, target, key, desc) {
|
|
452
453
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
453
454
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -486,15 +487,32 @@ var StaticFeatureGuard = class {
|
|
|
486
487
|
if (!required || required.length === 0) return true;
|
|
487
488
|
const request = context.switchToHttp().getRequest();
|
|
488
489
|
const user = request.user;
|
|
489
|
-
if (!user)
|
|
490
|
+
if (!user) {
|
|
491
|
+
throw new ForbiddenException2({
|
|
492
|
+
code: AUTH_ERROR_CODES.NOT_AUTHENTICATED,
|
|
493
|
+
message: "Not authenticated"
|
|
494
|
+
});
|
|
495
|
+
}
|
|
490
496
|
const role = this.config?.userRoleResolver ? this.config.userRoleResolver(user) : user.role ?? user.platformRole;
|
|
491
497
|
if (role === "SUPER_ADMIN") return true;
|
|
492
498
|
const tenantId = this.config?.tenantIdResolver ? this.config.tenantIdResolver(request) : request.tenantId ?? user.tenantId;
|
|
493
|
-
if (!tenantId)
|
|
499
|
+
if (!tenantId) {
|
|
500
|
+
throw new ForbiddenException2({
|
|
501
|
+
code: AUTH_ERROR_CODES.NO_TENANT_ASSIGNED,
|
|
502
|
+
message: "No tenant assigned"
|
|
503
|
+
});
|
|
504
|
+
}
|
|
494
505
|
const snap = await this.entitlements.snapshot(tenantId);
|
|
495
506
|
const allowed = required.some((f) => snap.features.includes(f));
|
|
496
507
|
if (!allowed) {
|
|
497
|
-
|
|
508
|
+
const body = {
|
|
509
|
+
code: FEATURE_NOT_LICENSED,
|
|
510
|
+
featureKey: required[0],
|
|
511
|
+
featureKeys: required,
|
|
512
|
+
offers: [],
|
|
513
|
+
message: `Feature ${required.join(" / ")} is not included in the current plan.`
|
|
514
|
+
};
|
|
515
|
+
throw new ForbiddenException2(body);
|
|
498
516
|
}
|
|
499
517
|
return true;
|
|
500
518
|
}
|
|
@@ -737,7 +755,7 @@ function buildTenantManifestController(options) {
|
|
|
737
755
|
async get(req) {
|
|
738
756
|
const tenantId = req.user?.tenantId ?? req.tenantId;
|
|
739
757
|
if (!tenantId) {
|
|
740
|
-
throw new Error("
|
|
758
|
+
throw new Error("No tenant on the request \u2014 TenantManifest requires user.tenantId.");
|
|
741
759
|
}
|
|
742
760
|
return this.svc.getManifest(tenantId);
|
|
743
761
|
}
|
|
@@ -3,7 +3,7 @@ import { ExecutionContext } from '@nestjs/common';
|
|
|
3
3
|
import { Reflector } from '@nestjs/core';
|
|
4
4
|
import { SuperAdminProvisioningPort, SetupStatusResponse, SetupRequest, SetupResult, SetupConfirmMfaRequest, SetupConfirmMfaResponse } from '@saasicat/types';
|
|
5
5
|
import { h as MfaService } from './admin-stats.service-Db2exT_d.js';
|
|
6
|
-
import { f as SaasPlatformModuleOptions } from './saas-platform.module-
|
|
6
|
+
import { f as SaasPlatformModuleOptions } from './saas-platform.module-DChhDrR-.js';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Shared metadata key for endpoints that intentionally run without application
|
|
@@ -3,7 +3,7 @@ import { ExecutionContext } from '@nestjs/common';
|
|
|
3
3
|
import { Reflector } from '@nestjs/core';
|
|
4
4
|
import { SuperAdminProvisioningPort, SetupStatusResponse, SetupRequest, SetupResult, SetupConfirmMfaRequest, SetupConfirmMfaResponse } from '@saasicat/types';
|
|
5
5
|
import { h as MfaService } from './admin-stats.service-HUZIU448.cjs';
|
|
6
|
-
import { f as SaasPlatformModuleOptions } from './saas-platform.module-
|
|
6
|
+
import { f as SaasPlatformModuleOptions } from './saas-platform.module-BfaYfL-G.cjs';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Shared metadata key for endpoints that intentionally run without application
|
|
@@ -117,7 +117,7 @@ interface ImplementsCapabilityMetadata extends ImplementsCapabilityOptions {
|
|
|
117
117
|
* ```ts
|
|
118
118
|
* @Post()
|
|
119
119
|
* @ImplementsCapability('invoice.create', {
|
|
120
|
-
* label: '
|
|
120
|
+
* label: 'Create invoice',
|
|
121
121
|
* feature: 'INVOICE_MANAGEMENT',
|
|
122
122
|
* kind: 'endpoint',
|
|
123
123
|
* owner: 'accounting',
|
|
@@ -157,7 +157,7 @@ declare const RequiresCapability: (...capabilityKeys: string[]) => _nestjs_commo
|
|
|
157
157
|
* @Injectable()
|
|
158
158
|
* @DefinesQuota({
|
|
159
159
|
* key: 'invoicesPerMonth',
|
|
160
|
-
* label: '
|
|
160
|
+
* label: 'Invoices per month',
|
|
161
161
|
* unit: 'invoices',
|
|
162
162
|
* policy: 'monthlyReset',
|
|
163
163
|
* feature: 'INVOICE_MANAGEMENT',
|
|
@@ -117,7 +117,7 @@ interface ImplementsCapabilityMetadata extends ImplementsCapabilityOptions {
|
|
|
117
117
|
* ```ts
|
|
118
118
|
* @Post()
|
|
119
119
|
* @ImplementsCapability('invoice.create', {
|
|
120
|
-
* label: '
|
|
120
|
+
* label: 'Create invoice',
|
|
121
121
|
* feature: 'INVOICE_MANAGEMENT',
|
|
122
122
|
* kind: 'endpoint',
|
|
123
123
|
* owner: 'accounting',
|
|
@@ -157,7 +157,7 @@ declare const RequiresCapability: (...capabilityKeys: string[]) => _nestjs_commo
|
|
|
157
157
|
* @Injectable()
|
|
158
158
|
* @DefinesQuota({
|
|
159
159
|
* key: 'invoicesPerMonth',
|
|
160
|
-
* label: '
|
|
160
|
+
* label: 'Invoices per month',
|
|
161
161
|
* unit: 'invoices',
|
|
162
162
|
* policy: 'monthlyReset',
|
|
163
163
|
* feature: 'INVOICE_MANAGEMENT',
|