@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
|
@@ -8,13 +8,16 @@ import {
|
|
|
8
8
|
PROMO_SUBSCRIPTION_LOOKUP_TOKEN,
|
|
9
9
|
PROMO_TRANSACTION_RUNNER_TOKEN,
|
|
10
10
|
PromoCodesService
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-VQQWF7MW.js";
|
|
12
12
|
import {
|
|
13
13
|
AdminAuditService
|
|
14
14
|
} from "./chunk-E56W4U2P.js";
|
|
15
15
|
import {
|
|
16
16
|
SaaSiCatPublicRoute
|
|
17
17
|
} from "./chunk-YSC46BGM.js";
|
|
18
|
+
import {
|
|
19
|
+
codedError
|
|
20
|
+
} from "./chunk-EBJJNTW6.js";
|
|
18
21
|
import {
|
|
19
22
|
asProvider
|
|
20
23
|
} from "./chunk-SABTXESR.js";
|
|
@@ -58,7 +61,7 @@ var PromoCodeExpirer = class _PromoCodeExpirer {
|
|
|
58
61
|
const codes = await this.promoRepo.expireDueCodes(now);
|
|
59
62
|
const redemptions = await this.redemptionRepo.expireDueRedemptions(now);
|
|
60
63
|
if (codes > 0 || redemptions > 0) {
|
|
61
|
-
this.logger.log(`PromoCodeExpirer: ${codes} Codes, ${redemptions}
|
|
64
|
+
this.logger.log(`PromoCodeExpirer: ${codes} Codes, ${redemptions} redemptions expired.`);
|
|
62
65
|
}
|
|
63
66
|
}
|
|
64
67
|
};
|
|
@@ -84,6 +87,7 @@ PromoCodeExpirer = _ts_decorate([
|
|
|
84
87
|
|
|
85
88
|
// src/promo/rate-limit.guard.ts
|
|
86
89
|
import { HttpException, HttpStatus, Injectable as Injectable2 } from "@nestjs/common";
|
|
90
|
+
import { REGISTRATION_ERROR_CODES } from "@saasicat/types";
|
|
87
91
|
function _ts_decorate2(decorators, target, key, desc) {
|
|
88
92
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
89
93
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -95,6 +99,18 @@ var IP_WINDOW_MS = 6e4;
|
|
|
95
99
|
var IP_LIMIT = 20;
|
|
96
100
|
var SESSION_WINDOW_MS = 60 * 6e4;
|
|
97
101
|
var SESSION_LIMIT = 50;
|
|
102
|
+
function rateLimited(windowMs) {
|
|
103
|
+
return new HttpException({
|
|
104
|
+
...codedError(REGISTRATION_ERROR_CODES.RATE_LIMITED, {
|
|
105
|
+
retryAfterSeconds: Math.ceil(windowMs / 1e3)
|
|
106
|
+
}),
|
|
107
|
+
retryAfterSeconds: Math.ceil(windowMs / 1e3),
|
|
108
|
+
valid: false,
|
|
109
|
+
// Superseded by `code` — kept until all consumers read `code`.
|
|
110
|
+
reason: "RATE_LIMITED"
|
|
111
|
+
}, HttpStatus.TOO_MANY_REQUESTS);
|
|
112
|
+
}
|
|
113
|
+
__name(rateLimited, "rateLimited");
|
|
98
114
|
var PromoCodeRateLimitGuard = class {
|
|
99
115
|
static {
|
|
100
116
|
__name(this, "PromoCodeRateLimitGuard");
|
|
@@ -106,16 +122,10 @@ var PromoCodeRateLimitGuard = class {
|
|
|
106
122
|
const ipKey = ipFingerprint(req);
|
|
107
123
|
const sessionKey = req.user?.id ?? null;
|
|
108
124
|
if (this.exceeded(this.ipBuckets, ipKey, IP_WINDOW_MS, IP_LIMIT)) {
|
|
109
|
-
throw
|
|
110
|
-
valid: false,
|
|
111
|
-
reason: "RATE_LIMITED"
|
|
112
|
-
}, HttpStatus.TOO_MANY_REQUESTS);
|
|
125
|
+
throw rateLimited(IP_WINDOW_MS);
|
|
113
126
|
}
|
|
114
127
|
if (sessionKey && this.exceeded(this.sessionBuckets, sessionKey, SESSION_WINDOW_MS, SESSION_LIMIT)) {
|
|
115
|
-
throw
|
|
116
|
-
valid: false,
|
|
117
|
-
reason: "RATE_LIMITED"
|
|
118
|
-
}, HttpStatus.TOO_MANY_REQUESTS);
|
|
128
|
+
throw rateLimited(SESSION_WINDOW_MS);
|
|
119
129
|
}
|
|
120
130
|
return true;
|
|
121
131
|
}
|
|
@@ -179,21 +189,21 @@ var PreviewPromoCodeDto = class {
|
|
|
179
189
|
_ts_decorate3([
|
|
180
190
|
IsString(),
|
|
181
191
|
Matches(CODE_PATTERN, {
|
|
182
|
-
message: 'code
|
|
192
|
+
message: 'code must consist of A\u2013Z, 0\u20139, "-" and "_" (4\u201332 characters)'
|
|
183
193
|
}),
|
|
184
194
|
_ts_metadata2("design:type", String)
|
|
185
195
|
], PreviewPromoCodeDto.prototype, "code", void 0);
|
|
186
196
|
_ts_decorate3([
|
|
187
197
|
IsString(),
|
|
188
198
|
Matches(PLAN_OR_CYCLE_PATTERN, {
|
|
189
|
-
message: "plan
|
|
199
|
+
message: "plan must be SCREAMING_SNAKE_CASE"
|
|
190
200
|
}),
|
|
191
201
|
_ts_metadata2("design:type", String)
|
|
192
202
|
], PreviewPromoCodeDto.prototype, "plan", void 0);
|
|
193
203
|
_ts_decorate3([
|
|
194
204
|
IsString(),
|
|
195
205
|
Matches(PLAN_OR_CYCLE_PATTERN, {
|
|
196
|
-
message: "billingCycle
|
|
206
|
+
message: "billingCycle must be SCREAMING_SNAKE_CASE (e.g. MONTHLY, YEARLY)"
|
|
197
207
|
}),
|
|
198
208
|
_ts_metadata2("design:type", String)
|
|
199
209
|
], PreviewPromoCodeDto.prototype, "billingCycle", void 0);
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DiscoverySnapshotNotFoundError,
|
|
3
3
|
loadDiscoverySnapshotFromFile
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-74SCJ4HF.js";
|
|
5
5
|
import {
|
|
6
6
|
ADVISORY_STRICT_MODE_CODES,
|
|
7
7
|
validateBundleDraft,
|
|
8
8
|
validatePlanDraft
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-GC5AKN44.js";
|
|
10
10
|
import {
|
|
11
11
|
__name
|
|
12
12
|
} from "./chunk-SHUYVCID.js";
|
|
@@ -66,9 +66,9 @@ function runPreflight(input) {
|
|
|
66
66
|
__name(runPreflight, "runPreflight");
|
|
67
67
|
function formatPreflightReport(report) {
|
|
68
68
|
const lines = [];
|
|
69
|
-
lines.push(`Preflight (Status: ${report.overall.toUpperCase()})`, ` Plans: ${report.counts.planFindings} \xB7 Bundles: ${report.counts.bundleFindings} \xB7
|
|
69
|
+
lines.push(`Preflight (Status: ${report.overall.toUpperCase()})`, ` Plans: ${report.counts.planFindings} \xB7 Bundles: ${report.counts.bundleFindings} \xB7 Total: ${report.counts.total}`, "");
|
|
70
70
|
if (report.findings.length === 0) {
|
|
71
|
-
lines.push(" \u2713
|
|
71
|
+
lines.push(" \u2713 No strict-mode violations found.");
|
|
72
72
|
return lines.join("\n");
|
|
73
73
|
}
|
|
74
74
|
let lastEntity = "";
|
|
@@ -138,9 +138,9 @@ function validateSeedAgainstSnapshot(input) {
|
|
|
138
138
|
__name(validateSeedAgainstSnapshot, "validateSeedAgainstSnapshot");
|
|
139
139
|
function formatSeedGateReport(report) {
|
|
140
140
|
const lines = [];
|
|
141
|
-
lines.push(`Seed-Gate (Status: ${report.overall.toUpperCase()})`, ` Plans: ${report.counts.planFindings} \xB7 Bundles: ${report.counts.bundleFindings} \xB7
|
|
141
|
+
lines.push(`Seed-Gate (Status: ${report.overall.toUpperCase()})`, ` Plans: ${report.counts.planFindings} \xB7 Bundles: ${report.counts.bundleFindings} \xB7 Total: ${report.counts.total}`, "");
|
|
142
142
|
if (report.findings.length === 0) {
|
|
143
|
-
lines.push(" \u2713
|
|
143
|
+
lines.push(" \u2713 Every seeded plan/bundle feature is discovered (and approved where checked).");
|
|
144
144
|
return lines.join("\n");
|
|
145
145
|
}
|
|
146
146
|
let lastEntity = "";
|
|
@@ -174,10 +174,10 @@ function runSeedGateFromFile(options) {
|
|
|
174
174
|
} catch (err) {
|
|
175
175
|
if (err instanceof DiscoverySnapshotNotFoundError) {
|
|
176
176
|
if (mode === "blocking") {
|
|
177
|
-
error(`[seed-gate]
|
|
177
|
+
error(`[seed-gate] ERROR: no discovery snapshot at ${options.snapshotPath} \u2014 required in blocking mode (run the headless scan before seeding, #23).`);
|
|
178
178
|
return exit(4);
|
|
179
179
|
}
|
|
180
|
-
warn(`[seed-gate]
|
|
180
|
+
warn(`[seed-gate] No discovery snapshot at ${options.snapshotPath} \u2014 skipped (report-only).`);
|
|
181
181
|
return null;
|
|
182
182
|
}
|
|
183
183
|
throw err;
|
|
@@ -191,10 +191,10 @@ function runSeedGateFromFile(options) {
|
|
|
191
191
|
log(formatSeedGateReport(report));
|
|
192
192
|
if (report.overall === "error") {
|
|
193
193
|
if (mode === "blocking") {
|
|
194
|
-
error("[seed-gate] blocking:
|
|
194
|
+
error("[seed-gate] blocking: violations found \u2014 aborting the seed (#23).");
|
|
195
195
|
return exit(seedGateExitCode(report));
|
|
196
196
|
}
|
|
197
|
-
warn("[seed-gate] report-only:
|
|
197
|
+
warn("[seed-gate] report-only: violations found, seeding continues anyway.");
|
|
198
198
|
}
|
|
199
199
|
return report;
|
|
200
200
|
}
|
|
@@ -74,7 +74,7 @@ var DiscoveryScanner = class _DiscoveryScanner {
|
|
|
74
74
|
writeFileSync(path, JSON.stringify(snapshot, null, 2) + "\n", "utf-8");
|
|
75
75
|
this.logger.log(`Discovery snapshot persisted to ${path}`);
|
|
76
76
|
} catch (err) {
|
|
77
|
-
this.logger.warn(`
|
|
77
|
+
this.logger.warn(`The discovery snapshot could not be written to ${path}: ${err.message}`);
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
/**
|
|
@@ -159,7 +159,7 @@ var DiscoveryScanner = class _DiscoveryScanner {
|
|
|
159
159
|
registerCapability(target, meta, declaredAt) {
|
|
160
160
|
const existing = target.get(meta.capabilityKey);
|
|
161
161
|
if (existing) {
|
|
162
|
-
this.logger.warn(`Capability '${meta.capabilityKey}'
|
|
162
|
+
this.logger.warn(`Capability '${meta.capabilityKey}' is declared in several places (${existing.declaredAt} and ${declaredAt}); the first declaration wins`);
|
|
163
163
|
return;
|
|
164
164
|
}
|
|
165
165
|
target.set(meta.capabilityKey, {
|
|
@@ -180,7 +180,7 @@ var DiscoveryScanner = class _DiscoveryScanner {
|
|
|
180
180
|
registerQuota(target, opts, declaredAt) {
|
|
181
181
|
const existing = target.get(opts.key);
|
|
182
182
|
if (existing) {
|
|
183
|
-
this.logger.warn(`Quota '${opts.key}'
|
|
183
|
+
this.logger.warn(`Quota '${opts.key}' is defined in several places (${existing.declaredAt} and ${declaredAt}); the first definition wins`);
|
|
184
184
|
return;
|
|
185
185
|
}
|
|
186
186
|
target.set(opts.key, {
|
|
@@ -11,7 +11,7 @@ var DiscoverySnapshotNotFoundError = class extends Error {
|
|
|
11
11
|
}
|
|
12
12
|
path;
|
|
13
13
|
constructor(path) {
|
|
14
|
-
super(`Discovery
|
|
14
|
+
super(`Discovery snapshot file not found: ${path}. Boot the app first with DISCOVERY_SNAPSHOT_PATH or DiscoveryModule.forRoot({ snapshotPath: '\u2026' }) so that the scanner writes the file on the OnApplicationBootstrap hook.`), this.path = path;
|
|
15
15
|
this.name = "DiscoverySnapshotNotFoundError";
|
|
16
16
|
}
|
|
17
17
|
};
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
PLAN_CATALOG_TOKEN,
|
|
3
3
|
isFeaturePlannedOnly
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-DQKCK7DX.js";
|
|
5
5
|
import {
|
|
6
6
|
SUBSCRIPTION_CONTRACT_REPOSITORY_TOKEN
|
|
7
7
|
} from "./chunk-NYLON2VC.js";
|
|
8
|
+
import {
|
|
9
|
+
codedError
|
|
10
|
+
} from "./chunk-EBJJNTW6.js";
|
|
8
11
|
import {
|
|
9
12
|
ENTITLEMENT_RESOLUTION_CONFIG_TOKEN,
|
|
10
13
|
ENTITLEMENT_SERVICE_TOKEN,
|
|
@@ -250,7 +253,7 @@ function assertNoReplacesCycle(index) {
|
|
|
250
253
|
...path.slice(cycleStart),
|
|
251
254
|
key
|
|
252
255
|
].join(" \u2192 ");
|
|
253
|
-
throw new Error(`replaces
|
|
256
|
+
throw new Error(`replaces cycle in the discovery metadata: ${cycle}. Feature replacements must be acyclic \u2014 fix the replaces declarations in code (#39).`);
|
|
254
257
|
}
|
|
255
258
|
state.set(key, IN_PROGRESS);
|
|
256
259
|
for (const successor of index.get(key) ?? []) {
|
|
@@ -294,7 +297,7 @@ var LimitExceededError = class extends Error {
|
|
|
294
297
|
used;
|
|
295
298
|
code = LIMIT_EXCEEDED;
|
|
296
299
|
constructor(dimension, max, used) {
|
|
297
|
-
super(`Limit
|
|
300
|
+
super(`Limit for ${dimension} reached: ${used}/${max}.`), this.dimension = dimension, this.max = max, this.used = used;
|
|
298
301
|
this.name = "LimitExceededError";
|
|
299
302
|
}
|
|
300
303
|
};
|
|
@@ -304,7 +307,8 @@ function isLimitExceededError(err) {
|
|
|
304
307
|
__name(isLimitExceededError, "isLimitExceededError");
|
|
305
308
|
|
|
306
309
|
// src/entitlement/service.ts
|
|
307
|
-
import { Inject, Injectable, NotFoundException, Optional } from "@nestjs/common";
|
|
310
|
+
import { Inject, Injectable, InternalServerErrorException, NotFoundException, Optional } from "@nestjs/common";
|
|
311
|
+
import { BILLING_ERROR_CODES } from "@saasicat/types";
|
|
308
312
|
|
|
309
313
|
// src/billing/subscription-bundles.tokens.ts
|
|
310
314
|
var SUBSCRIPTION_BUNDLE_REPOSITORY_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/SubscriptionBundleRepository");
|
|
@@ -371,7 +375,13 @@ var EntitlementService = class {
|
|
|
371
375
|
if (cached) return cached;
|
|
372
376
|
const sub = await this.subscriptions.findByTenantId(tenantId);
|
|
373
377
|
if (!sub) {
|
|
374
|
-
throw new NotFoundException(
|
|
378
|
+
throw new NotFoundException({
|
|
379
|
+
code: BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
|
|
380
|
+
message: `No subscription for tenant ${tenantId}`,
|
|
381
|
+
params: {
|
|
382
|
+
tenantId
|
|
383
|
+
}
|
|
384
|
+
});
|
|
375
385
|
}
|
|
376
386
|
const limits = await this.deriveLimits(sub, now);
|
|
377
387
|
this.writeCache(tenantId, limits, now.getTime());
|
|
@@ -444,7 +454,7 @@ var EntitlementService = class {
|
|
|
444
454
|
return Promise.all(active.map(async (booking) => {
|
|
445
455
|
const bv = await this.bundles.findVersionById(booking.bundleVersionId, tx);
|
|
446
456
|
if (!bv) {
|
|
447
|
-
throw new Error(`BundleVersion '${booking.bundleVersionId}'
|
|
457
|
+
throw new Error(`BundleVersion '${booking.bundleVersionId}' from an active subscription bundle not found`);
|
|
448
458
|
}
|
|
449
459
|
return {
|
|
450
460
|
bundleKey: bv.bundleKey,
|
|
@@ -466,12 +476,21 @@ var EntitlementService = class {
|
|
|
466
476
|
return this.tx.run(async (tx) => {
|
|
467
477
|
const sub = await this.subscriptions.findByTenantIdLocked(input.tenantId, tx);
|
|
468
478
|
if (!sub) {
|
|
469
|
-
throw new NotFoundException(
|
|
479
|
+
throw new NotFoundException({
|
|
480
|
+
code: BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
|
|
481
|
+
message: `No subscription for tenant ${input.tenantId}`,
|
|
482
|
+
params: {
|
|
483
|
+
tenantId: input.tenantId
|
|
484
|
+
}
|
|
485
|
+
});
|
|
470
486
|
}
|
|
471
487
|
const limits = await this.deriveLimits(sub, now, tx);
|
|
472
488
|
const max = limits.quotas[input.dimension];
|
|
473
489
|
if (max === void 0) {
|
|
474
|
-
throw new
|
|
490
|
+
throw new InternalServerErrorException(codedError(BILLING_ERROR_CODES.QUOTA_DIMENSION_UNKNOWN, {
|
|
491
|
+
dimension: input.dimension,
|
|
492
|
+
projectKey: this.catalog.projectKey
|
|
493
|
+
}));
|
|
475
494
|
}
|
|
476
495
|
const used = await input.currentUsage(tx, input.tenantId);
|
|
477
496
|
if (max !== -1 && used + delta > max) {
|
|
@@ -492,7 +511,15 @@ var EntitlementService = class {
|
|
|
492
511
|
async findActivePlanVersionOrFallback(planId, asOf, tx) {
|
|
493
512
|
const v = this.planVersions.findActive ? await this.planVersions.findActive(planId, asOf, tx) : await this.planVersions.findLatestLive(planId, tx);
|
|
494
513
|
if (!v) {
|
|
495
|
-
|
|
514
|
+
const asOfDate = asOf.toISOString().slice(0, 10);
|
|
515
|
+
throw new NotFoundException({
|
|
516
|
+
code: BILLING_ERROR_CODES.NO_ACTIVE_PLAN_VERSION,
|
|
517
|
+
message: `No plan version for ${planId} active as of ${asOfDate} \u2014 neither the validFrom window is satisfied nor is a latest-live version available.`,
|
|
518
|
+
params: {
|
|
519
|
+
planId,
|
|
520
|
+
asOf: asOfDate
|
|
521
|
+
}
|
|
522
|
+
});
|
|
496
523
|
}
|
|
497
524
|
return v;
|
|
498
525
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
appendImplicitDiscountLineItem
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-BSK6YBLI.js";
|
|
4
4
|
import {
|
|
5
5
|
SUBSCRIPTION_CONTRACT_REPOSITORY_TOKEN
|
|
6
6
|
} from "./chunk-NYLON2VC.js";
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
|
|
14
14
|
// src/subscription-contract/subscription-contract.service.ts
|
|
15
15
|
import { ConflictException, Inject, Injectable, NotFoundException, UnprocessableEntityException } from "@nestjs/common";
|
|
16
|
+
import { CONTRACT_ERROR_CODES } from "@saasicat/types";
|
|
16
17
|
function _ts_decorate(decorators, target, key, desc) {
|
|
17
18
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
18
19
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -44,7 +45,13 @@ var SubscriptionContractService = class {
|
|
|
44
45
|
async getById(contractId) {
|
|
45
46
|
const row = await this.repo.findById(contractId);
|
|
46
47
|
if (!row) {
|
|
47
|
-
throw new NotFoundException(
|
|
48
|
+
throw new NotFoundException({
|
|
49
|
+
code: CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_NOT_FOUND,
|
|
50
|
+
message: `SubscriptionContract '${contractId}' not found`,
|
|
51
|
+
params: {
|
|
52
|
+
contractId
|
|
53
|
+
}
|
|
54
|
+
});
|
|
48
55
|
}
|
|
49
56
|
return row;
|
|
50
57
|
}
|
|
@@ -54,7 +61,14 @@ var SubscriptionContractService = class {
|
|
|
54
61
|
async getActiveInvoiceSnapshotForTenant(tenantId, asOf = /* @__PURE__ */ new Date()) {
|
|
55
62
|
const contract = await this.repo.findActiveByTenantId(tenantId, asOf);
|
|
56
63
|
if (!contract) {
|
|
57
|
-
throw new NotFoundException(
|
|
64
|
+
throw new NotFoundException({
|
|
65
|
+
code: CONTRACT_ERROR_CODES.NO_ACTIVE_SUBSCRIPTION_CONTRACT,
|
|
66
|
+
message: `No active subscription contract for tenant ${tenantId}`,
|
|
67
|
+
params: {
|
|
68
|
+
tenantId,
|
|
69
|
+
asOf: asOf.toISOString()
|
|
70
|
+
}
|
|
71
|
+
});
|
|
58
72
|
}
|
|
59
73
|
return subscriptionContractToInvoiceSnapshot(contract);
|
|
60
74
|
}
|
|
@@ -87,7 +101,14 @@ var SubscriptionContractService = class {
|
|
|
87
101
|
}
|
|
88
102
|
createDataFromOffer(offer, options) {
|
|
89
103
|
if (offer.status !== "consumed") {
|
|
90
|
-
throw new ConflictException(
|
|
104
|
+
throw new ConflictException({
|
|
105
|
+
code: CONTRACT_ERROR_CODES.CHECKOUT_OFFER_NOT_CONSUMED,
|
|
106
|
+
message: `CheckoutOffer '${offer.id}' must be consumed before the contract is created`,
|
|
107
|
+
params: {
|
|
108
|
+
offerId: offer.id,
|
|
109
|
+
status: offer.status
|
|
110
|
+
}
|
|
111
|
+
});
|
|
91
112
|
}
|
|
92
113
|
const lineItems = this.lineItemsFromOffer(offer);
|
|
93
114
|
return {
|
|
@@ -120,8 +141,8 @@ var SubscriptionContractService = class {
|
|
|
120
141
|
const source = offer.lineItems ?? [];
|
|
121
142
|
if (source.length === 0) {
|
|
122
143
|
throw new UnprocessableEntityException({
|
|
123
|
-
code:
|
|
124
|
-
message: "
|
|
144
|
+
code: CONTRACT_ERROR_CODES.CHECKOUT_OFFER_LINE_ITEMS_REQUIRED,
|
|
145
|
+
message: "A checkout offer can yield only one contract, and only once its line items are frozen."
|
|
125
146
|
});
|
|
126
147
|
}
|
|
127
148
|
return appendImplicitDiscountLineItem({
|
|
@@ -159,8 +180,11 @@ var SubscriptionContractService = class {
|
|
|
159
180
|
const date = value instanceof Date ? new Date(value) : new Date(value);
|
|
160
181
|
if (Number.isNaN(date.getTime())) {
|
|
161
182
|
throw new UnprocessableEntityException({
|
|
162
|
-
code:
|
|
163
|
-
message: `${field}
|
|
183
|
+
code: CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_INVALID_DATE,
|
|
184
|
+
message: `${field} must be a valid date.`,
|
|
185
|
+
params: {
|
|
186
|
+
field
|
|
187
|
+
}
|
|
164
188
|
});
|
|
165
189
|
}
|
|
166
190
|
return date;
|
|
@@ -180,32 +204,39 @@ var SubscriptionContractService = class {
|
|
|
180
204
|
assertCreateData(data) {
|
|
181
205
|
if (data.lineItems.length === 0) {
|
|
182
206
|
throw new UnprocessableEntityException({
|
|
183
|
-
code:
|
|
184
|
-
message: "
|
|
207
|
+
code: CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_LINE_ITEMS_REQUIRED,
|
|
208
|
+
message: "A subscription contract requires at least one line item."
|
|
185
209
|
});
|
|
186
210
|
}
|
|
187
211
|
if (data.effectiveUntil && data.effectiveUntil <= data.effectiveFrom) {
|
|
188
212
|
throw new UnprocessableEntityException({
|
|
189
|
-
code:
|
|
190
|
-
message: "effectiveUntil
|
|
213
|
+
code: CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_INVALID_WINDOW,
|
|
214
|
+
message: "effectiveUntil must be after effectiveFrom."
|
|
191
215
|
});
|
|
192
216
|
}
|
|
193
217
|
const planLineItems = data.lineItems.filter((item) => item.kind === "plan");
|
|
194
218
|
if (planLineItems.length !== 1) {
|
|
195
219
|
throw new UnprocessableEntityException({
|
|
196
|
-
code:
|
|
197
|
-
message: "
|
|
220
|
+
code: CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_PLAN_LINE_ITEM_REQUIRED,
|
|
221
|
+
message: "A subscription contract requires exactly one plan base item."
|
|
198
222
|
});
|
|
199
223
|
}
|
|
200
224
|
}
|
|
201
225
|
assertTerminable(existing, data) {
|
|
202
226
|
if (existing.status === "terminated" || existing.status === "superseded") {
|
|
203
|
-
throw new ConflictException(
|
|
227
|
+
throw new ConflictException({
|
|
228
|
+
code: CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_ALREADY_CLOSED,
|
|
229
|
+
message: `SubscriptionContract '${existing.id}' is already closed`,
|
|
230
|
+
params: {
|
|
231
|
+
contractId: existing.id,
|
|
232
|
+
status: existing.status
|
|
233
|
+
}
|
|
234
|
+
});
|
|
204
235
|
}
|
|
205
236
|
if (data.effectiveUntil <= existing.effectiveFrom) {
|
|
206
237
|
throw new UnprocessableEntityException({
|
|
207
|
-
code:
|
|
208
|
-
message: "effectiveUntil
|
|
238
|
+
code: CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_TERMINATION_BEFORE_START,
|
|
239
|
+
message: "effectiveUntil must be after the effectiveFrom of the contract."
|
|
209
240
|
});
|
|
210
241
|
}
|
|
211
242
|
}
|
|
@@ -40,7 +40,7 @@ function createDiscountLineItem(input, discountNet) {
|
|
|
40
40
|
kind: "discount",
|
|
41
41
|
sourceKey: promoCode?.code ?? firstPromotion?.id ?? "price-discount",
|
|
42
42
|
sourceVersionId: null,
|
|
43
|
-
titleSnapshot: promoCode?.label ?? firstPromotion?.label ?? "
|
|
43
|
+
titleSnapshot: promoCode?.label ?? firstPromotion?.label ?? "Discount",
|
|
44
44
|
descriptionSnapshot: promoCode ? `Promo-Code ${promoCode.code}` : null,
|
|
45
45
|
quantity: 1,
|
|
46
46
|
unit: null,
|
|
@@ -152,7 +152,7 @@ __name(findPlan, "findPlan");
|
|
|
152
152
|
function getPlanOrThrow(catalog, planId) {
|
|
153
153
|
const plan = findPlan(catalog, planId);
|
|
154
154
|
if (!plan) {
|
|
155
|
-
throw new Error(`Plan "${planId}"
|
|
155
|
+
throw new Error(`Plan "${planId}" not found in the catalog (${catalog.projectKey})`);
|
|
156
156
|
}
|
|
157
157
|
return plan;
|
|
158
158
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__name
|
|
3
|
+
} from "./chunk-SHUYVCID.js";
|
|
4
|
+
|
|
5
|
+
// src/errors/coded-error.ts
|
|
6
|
+
import { ERROR_MESSAGES_EN, formatErrorMessage } from "@saasicat/types";
|
|
7
|
+
function codedError(code, params) {
|
|
8
|
+
const message = formatErrorMessage(ERROR_MESSAGES_EN[code], params ?? {});
|
|
9
|
+
return params ? {
|
|
10
|
+
code,
|
|
11
|
+
message,
|
|
12
|
+
params
|
|
13
|
+
} : {
|
|
14
|
+
code,
|
|
15
|
+
message
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
__name(codedError, "codedError");
|
|
19
|
+
|
|
20
|
+
export {
|
|
21
|
+
codedError
|
|
22
|
+
};
|