@saasicat/nest 0.26.0 → 0.27.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/LICENSE +90 -201
- package/dist/_entries.cjs +3265 -2846
- package/dist/billing/index.d.cts +4 -4
- package/dist/billing/index.d.ts +4 -4
- package/dist/billing/index.js +33 -31
- package/dist/catalog/index.js +5 -5
- package/dist/checkout-offer/index.js +2 -2
- package/dist/chunk-2FR6ZL7R.js +0 -0
- package/dist/chunk-2JTISFUZ.js +792 -0
- package/dist/chunk-2SLTRKXC.js +0 -0
- package/dist/chunk-3E6ONOMG.js +50 -0
- package/dist/{chunk-UOWK2HYG.js → chunk-7ZPFQXHM.js} +1 -1
- package/dist/{chunk-B7NRY2LV.js → chunk-CJFAOD7K.js} +1 -39
- package/dist/{chunk-J62YI25C.js → chunk-EHKMPP6J.js} +1194 -775
- package/dist/{chunk-RPO4IVUY.js → chunk-F4B2NJUZ.js} +26 -91
- package/dist/chunk-H4DOTO7S.js +85 -0
- package/dist/{chunk-V3MRZ7SH.js → chunk-UHD6FJBQ.js} +421 -1197
- package/dist/{define-saasicat-BK6RRWWG.d.ts → define-saasicat-3ois8t8s.d.ts} +26 -3
- package/dist/{define-saasicat-D30I-WET.d.cts → define-saasicat-CFxut1VR.d.cts} +26 -3
- package/dist/{enforce-quota.interceptor-B9QueAFf.d.ts → enforce-quota.interceptor-CYDNvAQz.d.ts} +3 -3
- package/dist/{enforce-quota.interceptor-DY6CLjoP.d.cts → enforce-quota.interceptor-N80KC34N.d.cts} +3 -3
- package/dist/entitlement/index.js +7 -4
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +87 -79
- package/dist/{saas-platform.module-D1mUZPrz.d.ts → module-options-ByJL7FZm.d.ts} +40 -27
- package/dist/{saas-platform.module-CgalxzcT.d.cts → module-options-klrPnWGF.d.cts} +40 -27
- package/dist/platform/index.d.cts +201 -19
- package/dist/platform/index.d.ts +201 -19
- package/dist/platform/index.js +58 -33
- package/dist/promo/index.js +2 -2
- package/dist/subscription-contract/index.js +5 -2
- package/dist/{tenant-billing.controller-DHSUrT_N.d.cts → tenant-billing.controller-CAMMdfn1.d.cts} +1 -1
- package/dist/{tenant-billing.controller-BSlpFqVA.d.ts → tenant-billing.controller-CYYK0CpT.d.ts} +1 -1
- package/dist/{subscription-bundles.module-BDbODWvA.d.ts → tenant-billing.module-B0stLoJn.d.ts} +51 -51
- package/dist/{subscription-bundles.module-Cfi-lFWF.d.cts → tenant-billing.module-BxJS6fX4.d.cts} +51 -51
- package/dist/testing/index.d.cts +8 -8
- package/dist/testing/index.d.ts +8 -8
- package/dist/testing/index.js +13 -11
- package/package.json +4 -4
- package/dist/{chunk-QB7MICKQ.js → chunk-7X5MSAIL.js} +9 -9
- package/dist/{chunk-FY4EJTV4.js → chunk-OYNHOY45.js} +0 -0
- package/dist/{chunk-I4XD43AN.js → chunk-XTVAFJ3P.js} +3 -3
|
@@ -0,0 +1,792 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FEATURE_GUARD_MARKER,
|
|
3
|
+
PUBLIC_CATALOG_BUNDLE_REPOSITORY_TOKEN,
|
|
4
|
+
PUBLIC_CATALOG_CATALOG_ENTRY_REPOSITORY_TOKEN,
|
|
5
|
+
PUBLIC_CATALOG_PROJECT_KEY_TOKEN,
|
|
6
|
+
REQUIRE_FEATURE_KEY,
|
|
7
|
+
periodEndAfter
|
|
8
|
+
} from "./chunk-UHD6FJBQ.js";
|
|
9
|
+
import {
|
|
10
|
+
EntitlementService,
|
|
11
|
+
isLimitExceededError
|
|
12
|
+
} from "./chunk-F4B2NJUZ.js";
|
|
13
|
+
import {
|
|
14
|
+
ENTITLEMENT_SERVICE_TOKEN
|
|
15
|
+
} from "./chunk-WKVBNTYC.js";
|
|
16
|
+
import {
|
|
17
|
+
asProvider
|
|
18
|
+
} from "./chunk-SABTXESR.js";
|
|
19
|
+
import {
|
|
20
|
+
__name
|
|
21
|
+
} from "./chunk-SHUYVCID.js";
|
|
22
|
+
|
|
23
|
+
// src/billing/version-publish.ts
|
|
24
|
+
var PublishValidationError = class extends Error {
|
|
25
|
+
static {
|
|
26
|
+
__name(this, "PublishValidationError");
|
|
27
|
+
}
|
|
28
|
+
code;
|
|
29
|
+
constructor(code, message) {
|
|
30
|
+
super(message), this.code = code;
|
|
31
|
+
this.name = "PublishValidationError";
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
function assertChangeNote(changeNote) {
|
|
35
|
+
const trimmed = (changeNote ?? "").trim();
|
|
36
|
+
if (trimmed.length === 0) {
|
|
37
|
+
throw new PublishValidationError("CHANGE_NOTE_REQUIRED", "changeNote is required when publishing a version.");
|
|
38
|
+
}
|
|
39
|
+
return trimmed;
|
|
40
|
+
}
|
|
41
|
+
__name(assertChangeNote, "assertChangeNote");
|
|
42
|
+
function assertDraftPublishable(draft, draftId) {
|
|
43
|
+
if (!draft) {
|
|
44
|
+
throw new PublishValidationError("NOT_FOUND", `PlanVersion ${draftId} not found`);
|
|
45
|
+
}
|
|
46
|
+
if (draft.publishedAt !== null) {
|
|
47
|
+
throw new PublishValidationError("ALREADY_PUBLISHED", `PlanVersion ${draftId} has already been published.`);
|
|
48
|
+
}
|
|
49
|
+
if (!draft.baseVersionId) {
|
|
50
|
+
throw new PublishValidationError("NO_BASE_VERSION", `Draft ${draftId} has no baseVersion \u2014 probably a manually created v1 that may only be published via a migration.`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
__name(assertDraftPublishable, "assertDraftPublishable");
|
|
54
|
+
function assertBaseVersionFresh(base, baseId, planContext) {
|
|
55
|
+
if (!base) {
|
|
56
|
+
throw new PublishValidationError("BASE_NOT_FOUND", `baseVersion ${baseId} no longer exists.`);
|
|
57
|
+
}
|
|
58
|
+
if (base.supersededAt !== null) {
|
|
59
|
+
throw new PublishValidationError("BASE_SUPERSEDED", `The draft is based on a superseded version (${planContext}). Rebase the draft onto the current live version, or create a new one.`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
__name(assertBaseVersionFresh, "assertBaseVersionFresh");
|
|
63
|
+
function assertOptimisticLockHeld(updateCount, draftId) {
|
|
64
|
+
if (updateCount !== 1) {
|
|
65
|
+
throw new PublishValidationError("OPTIMISTIC_LOCK_CONFLICT", `Draft ${draftId} has meanwhile been published by another admin.`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
__name(assertOptimisticLockHeld, "assertOptimisticLockHeld");
|
|
69
|
+
|
|
70
|
+
// src/billing/version-renewal.ts
|
|
71
|
+
function decideRenewal(sub, now) {
|
|
72
|
+
if (!sub.pendingPlanVersionId || !sub.pendingPlanVersionEffectiveAt) return "SKIP";
|
|
73
|
+
if (sub.pendingPlanVersionEffectiveAt > now) return "SKIP";
|
|
74
|
+
if (sub.pendingPlanVersionNonRegressive || sub.pendingPlanVersionAccepted) {
|
|
75
|
+
return "ROLL_FORWARD";
|
|
76
|
+
}
|
|
77
|
+
return "CLEAR_PENDING";
|
|
78
|
+
}
|
|
79
|
+
__name(decideRenewal, "decideRenewal");
|
|
80
|
+
function clearPendingPlanVersionFields() {
|
|
81
|
+
return {
|
|
82
|
+
pendingPlanVersionId: null,
|
|
83
|
+
pendingPlanVersionEffectiveAt: null,
|
|
84
|
+
pendingPlanVersionAccepted: false,
|
|
85
|
+
pendingPlanVersionAcceptedAt: null,
|
|
86
|
+
pendingPlanVersionAcceptedByUserId: null,
|
|
87
|
+
pendingPlanVersionNotifiedAt: null,
|
|
88
|
+
pendingPlanVersionReminderSentAt: null
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
__name(clearPendingPlanVersionFields, "clearPendingPlanVersionFields");
|
|
92
|
+
function computeNextPeriod(sub, now) {
|
|
93
|
+
if (sub.canceledAt !== null) return null;
|
|
94
|
+
if (sub.currentPeriodEnd === null) return null;
|
|
95
|
+
if (sub.currentPeriodEnd > now) return null;
|
|
96
|
+
const newStart = sub.currentPeriodEnd;
|
|
97
|
+
const newEnd = periodEndAfter(newStart, sub.billingCycle, now);
|
|
98
|
+
return {
|
|
99
|
+
currentPeriodStart: newStart,
|
|
100
|
+
currentPeriodEnd: newEnd
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
__name(computeNextPeriod, "computeNextPeriod");
|
|
104
|
+
|
|
105
|
+
// src/billing/feature-guard.tokens.ts
|
|
106
|
+
var FEATURE_GUARD_CONFIG_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/FeatureGuardConfig");
|
|
107
|
+
|
|
108
|
+
// src/billing/feature.guard.ts
|
|
109
|
+
import { ForbiddenException, Inject, Injectable, Logger, Optional } from "@nestjs/common";
|
|
110
|
+
import { Reflector } from "@nestjs/core";
|
|
111
|
+
import { AUTH_ERROR_CODES, FEATURE_NOT_LICENSED } from "@saasicat/types";
|
|
112
|
+
|
|
113
|
+
// src/billing/upsell.tokens.ts
|
|
114
|
+
var UPSELL_OFFER_RESOLVER_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/UpsellOfferResolver");
|
|
115
|
+
var UPSELL_OFFER_CURRENCY_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/UpsellOfferCurrency");
|
|
116
|
+
|
|
117
|
+
// src/billing/feature.guard.ts
|
|
118
|
+
function _ts_decorate(decorators, target, key, desc) {
|
|
119
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
120
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
121
|
+
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;
|
|
122
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
123
|
+
}
|
|
124
|
+
__name(_ts_decorate, "_ts_decorate");
|
|
125
|
+
function _ts_metadata(k, v) {
|
|
126
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
127
|
+
}
|
|
128
|
+
__name(_ts_metadata, "_ts_metadata");
|
|
129
|
+
function _ts_param(paramIndex, decorator) {
|
|
130
|
+
return function(target, key) {
|
|
131
|
+
decorator(target, key, paramIndex);
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
__name(_ts_param, "_ts_param");
|
|
135
|
+
var FeatureGuard = class _FeatureGuard {
|
|
136
|
+
static {
|
|
137
|
+
__name(this, "FeatureGuard");
|
|
138
|
+
}
|
|
139
|
+
reflector;
|
|
140
|
+
entitlements;
|
|
141
|
+
config;
|
|
142
|
+
upsellResolver;
|
|
143
|
+
/** Lets `EnforcementChainCheck` recognise this guard across CJS entries. */
|
|
144
|
+
static [FEATURE_GUARD_MARKER] = true;
|
|
145
|
+
logger = new Logger(_FeatureGuard.name);
|
|
146
|
+
constructor(reflector, entitlements, config = null, upsellResolver = null) {
|
|
147
|
+
this.reflector = reflector;
|
|
148
|
+
this.entitlements = entitlements;
|
|
149
|
+
this.config = config;
|
|
150
|
+
this.upsellResolver = upsellResolver;
|
|
151
|
+
}
|
|
152
|
+
async canActivate(context) {
|
|
153
|
+
const required = this.reflector.getAllAndOverride(REQUIRE_FEATURE_KEY, [
|
|
154
|
+
context.getHandler(),
|
|
155
|
+
context.getClass()
|
|
156
|
+
]);
|
|
157
|
+
if (!required || required.length === 0) return true;
|
|
158
|
+
const request = context.switchToHttp().getRequest();
|
|
159
|
+
const user = request.user;
|
|
160
|
+
if (!user) {
|
|
161
|
+
throw new ForbiddenException({
|
|
162
|
+
code: AUTH_ERROR_CODES.NOT_AUTHENTICATED,
|
|
163
|
+
message: "Not authenticated"
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
const role = this.config?.userRoleResolver ? this.config.userRoleResolver(user) : user.role ?? user.platformRole;
|
|
167
|
+
if (role === "SUPER_ADMIN") return true;
|
|
168
|
+
const tenantId = this.config?.tenantIdResolver ? this.config.tenantIdResolver(request) : request.tenantId ?? user.tenantId;
|
|
169
|
+
if (!tenantId) {
|
|
170
|
+
throw new ForbiddenException({
|
|
171
|
+
code: AUTH_ERROR_CODES.NO_TENANT_ASSIGNED,
|
|
172
|
+
message: "No tenant assigned"
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
const compute = /* @__PURE__ */ __name(() => this.entitlements.computeLimits(tenantId), "compute");
|
|
176
|
+
const limits = this.config?.tenantContextRunner ? await this.config.tenantContextRunner(tenantId, compute) : await compute();
|
|
177
|
+
const allowed = required.some((f) => limits.features.has(f));
|
|
178
|
+
if (!allowed) {
|
|
179
|
+
throw await this.buildNotLicensedException(required, tenantId);
|
|
180
|
+
}
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Upsell response (#36): the 403 body is always the full
|
|
185
|
+
* `FeatureNotLicensedBody`, so a consumer that matches on `code` can read
|
|
186
|
+
* `featureKey`/`offers` unconditionally. Without a registered
|
|
187
|
+
* `UpsellOfferResolver` there is simply nothing to offer — `offers` is
|
|
188
|
+
* empty, the same degradation a failing resolver produces.
|
|
189
|
+
*
|
|
190
|
+
* Deliberately 403 + `code` field instead of 402 — rationale in
|
|
191
|
+
* `@saasicat/types` upsell.types.ts (402 is reserved / inconsistently
|
|
192
|
+
* supported; SPA interceptors must not treat the 403 as an auth error, the
|
|
193
|
+
* distinction runs through `code`).
|
|
194
|
+
*
|
|
195
|
+
* The message must keep starting with the word `Feature` — consumers
|
|
196
|
+
* classify the response by that prefix.
|
|
197
|
+
*/
|
|
198
|
+
async buildNotLicensedException(required, tenantId) {
|
|
199
|
+
const body = {
|
|
200
|
+
code: FEATURE_NOT_LICENSED,
|
|
201
|
+
featureKey: required[0],
|
|
202
|
+
featureKeys: required,
|
|
203
|
+
offers: this.upsellResolver ? await this.resolveOffersSafe(required, tenantId) : [],
|
|
204
|
+
message: `Feature ${required.join(" / ")} is not included in the current plan.`
|
|
205
|
+
};
|
|
206
|
+
return new ForbiddenException(body);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* A resolver error must not turn the semantically correct 403 into a 500 —
|
|
210
|
+
* degrade to empty offers and log.
|
|
211
|
+
*/
|
|
212
|
+
async resolveOffersSafe(required, tenantId) {
|
|
213
|
+
try {
|
|
214
|
+
return await this.upsellResolver.resolveOffers(required, tenantId);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
this.logger.warn(`UpsellOfferResolver failed for [${required.join(", ")}]: ${String(error)}`);
|
|
217
|
+
return [];
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
FeatureGuard = _ts_decorate([
|
|
222
|
+
Injectable(),
|
|
223
|
+
_ts_param(0, Inject(Reflector)),
|
|
224
|
+
_ts_param(1, Inject(ENTITLEMENT_SERVICE_TOKEN)),
|
|
225
|
+
_ts_param(2, Optional()),
|
|
226
|
+
_ts_param(2, Inject(FEATURE_GUARD_CONFIG_TOKEN)),
|
|
227
|
+
_ts_param(3, Optional()),
|
|
228
|
+
_ts_param(3, Inject(UPSELL_OFFER_RESOLVER_TOKEN)),
|
|
229
|
+
_ts_metadata("design:type", Function),
|
|
230
|
+
_ts_metadata("design:paramtypes", [
|
|
231
|
+
typeof Reflector === "undefined" ? Object : Reflector,
|
|
232
|
+
typeof EntitlementService === "undefined" ? Object : EntitlementService,
|
|
233
|
+
Object,
|
|
234
|
+
Object
|
|
235
|
+
])
|
|
236
|
+
], FeatureGuard);
|
|
237
|
+
|
|
238
|
+
// src/billing/catalog-bundle-upsell-resolver.ts
|
|
239
|
+
import { Inject as Inject2, Injectable as Injectable2, Optional as Optional2 } from "@nestjs/common";
|
|
240
|
+
import { buildFeatureRequiresIndex, collectUnsatisfiedRequires } from "@saasicat/types";
|
|
241
|
+
function _ts_decorate2(decorators, target, key, desc) {
|
|
242
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
243
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
244
|
+
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;
|
|
245
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
246
|
+
}
|
|
247
|
+
__name(_ts_decorate2, "_ts_decorate");
|
|
248
|
+
function _ts_metadata2(k, v) {
|
|
249
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
250
|
+
}
|
|
251
|
+
__name(_ts_metadata2, "_ts_metadata");
|
|
252
|
+
function _ts_param2(paramIndex, decorator) {
|
|
253
|
+
return function(target, key) {
|
|
254
|
+
decorator(target, key, paramIndex);
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
__name(_ts_param2, "_ts_param");
|
|
258
|
+
var DEFAULT_CURRENCY = "EUR";
|
|
259
|
+
var CatalogBundleUpsellResolver = class {
|
|
260
|
+
static {
|
|
261
|
+
__name(this, "CatalogBundleUpsellResolver");
|
|
262
|
+
}
|
|
263
|
+
bundleRepo;
|
|
264
|
+
projectKey;
|
|
265
|
+
catalogEntryRepo;
|
|
266
|
+
currency;
|
|
267
|
+
constructor(bundleRepo, projectKey, catalogEntryRepo = null, currency = null) {
|
|
268
|
+
this.bundleRepo = bundleRepo;
|
|
269
|
+
this.projectKey = projectKey;
|
|
270
|
+
this.catalogEntryRepo = catalogEntryRepo;
|
|
271
|
+
this.currency = currency;
|
|
272
|
+
}
|
|
273
|
+
async resolveOffers(featureKeys, tenantId) {
|
|
274
|
+
void tenantId;
|
|
275
|
+
if (featureKeys.length === 0) return [];
|
|
276
|
+
const missing = new Set(featureKeys);
|
|
277
|
+
const wanted = /* @__PURE__ */ new Set([
|
|
278
|
+
...featureKeys,
|
|
279
|
+
...await this.lookupUnmetRequires(featureKeys)
|
|
280
|
+
]);
|
|
281
|
+
const ranked = [];
|
|
282
|
+
for (const live of await this.listLiveMarketedBundles()) {
|
|
283
|
+
const features = live.features;
|
|
284
|
+
if (!features.some((key) => missing.has(key))) continue;
|
|
285
|
+
ranked.push({
|
|
286
|
+
offer: this.toOffer(live),
|
|
287
|
+
coverage: features.filter((key) => wanted.has(key)).length
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
return ranked.sort(compareRankedOffers).map((r) => r.offer);
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Uncovered dependencies of the missing features (#35) — feed into the
|
|
294
|
+
* coverage ranking. Without a CatalogEntryRepository: empty (ranking
|
|
295
|
+
* degrades to price).
|
|
296
|
+
*/
|
|
297
|
+
async lookupUnmetRequires(featureKeys) {
|
|
298
|
+
if (!this.catalogEntryRepo) return [];
|
|
299
|
+
const rows = await this.catalogEntryRepo.listFeatures({
|
|
300
|
+
projectKey: this.projectKey
|
|
301
|
+
});
|
|
302
|
+
return collectUnsatisfiedRequires(featureKeys, buildFeatureRequiresIndex(rows));
|
|
303
|
+
}
|
|
304
|
+
/** Only published-and-live AND marketed — non-marketed bundles are not an offer. */
|
|
305
|
+
async listLiveMarketedBundles() {
|
|
306
|
+
const stems = await this.bundleRepo.list({
|
|
307
|
+
projectKey: this.projectKey,
|
|
308
|
+
excludeDeleted: true
|
|
309
|
+
});
|
|
310
|
+
const out = [];
|
|
311
|
+
for (const stem of stems) {
|
|
312
|
+
const live = await this.bundleRepo.findLatestLive(stem.id);
|
|
313
|
+
if (live?.marketed) out.push(live);
|
|
314
|
+
}
|
|
315
|
+
return out;
|
|
316
|
+
}
|
|
317
|
+
toOffer(live) {
|
|
318
|
+
return {
|
|
319
|
+
bundleKey: live.bundleKey,
|
|
320
|
+
bundleVersionId: live.id,
|
|
321
|
+
priceMonthlyNet: parsePrice(live.monthlyNet),
|
|
322
|
+
currency: this.currency ?? DEFAULT_CURRENCY,
|
|
323
|
+
label: live.label
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
CatalogBundleUpsellResolver = _ts_decorate2([
|
|
328
|
+
Injectable2(),
|
|
329
|
+
_ts_param2(0, Inject2(PUBLIC_CATALOG_BUNDLE_REPOSITORY_TOKEN)),
|
|
330
|
+
_ts_param2(1, Inject2(PUBLIC_CATALOG_PROJECT_KEY_TOKEN)),
|
|
331
|
+
_ts_param2(2, Optional2()),
|
|
332
|
+
_ts_param2(2, Inject2(PUBLIC_CATALOG_CATALOG_ENTRY_REPOSITORY_TOKEN)),
|
|
333
|
+
_ts_param2(3, Optional2()),
|
|
334
|
+
_ts_param2(3, Inject2(UPSELL_OFFER_CURRENCY_TOKEN)),
|
|
335
|
+
_ts_metadata2("design:type", Function),
|
|
336
|
+
_ts_metadata2("design:paramtypes", [
|
|
337
|
+
typeof BundleRepository === "undefined" ? Object : BundleRepository,
|
|
338
|
+
String,
|
|
339
|
+
Object,
|
|
340
|
+
Object
|
|
341
|
+
])
|
|
342
|
+
], CatalogBundleUpsellResolver);
|
|
343
|
+
function parsePrice(decimal) {
|
|
344
|
+
if (decimal === null) return null;
|
|
345
|
+
const n = Number.parseFloat(decimal);
|
|
346
|
+
return Number.isFinite(n) ? n : null;
|
|
347
|
+
}
|
|
348
|
+
__name(parsePrice, "parsePrice");
|
|
349
|
+
function compareRankedOffers(a, b) {
|
|
350
|
+
if (a.coverage !== b.coverage) return b.coverage - a.coverage;
|
|
351
|
+
const priceA = a.offer.priceMonthlyNet ?? Number.POSITIVE_INFINITY;
|
|
352
|
+
const priceB = b.offer.priceMonthlyNet ?? Number.POSITIVE_INFINITY;
|
|
353
|
+
if (priceA !== priceB) return priceA - priceB;
|
|
354
|
+
return a.offer.bundleKey.localeCompare(b.offer.bundleKey);
|
|
355
|
+
}
|
|
356
|
+
__name(compareRankedOffers, "compareRankedOffers");
|
|
357
|
+
|
|
358
|
+
// src/billing/limit-exceeded.filter.ts
|
|
359
|
+
import { Catch, HttpStatus, Logger as Logger2 } from "@nestjs/common";
|
|
360
|
+
import { BaseExceptionFilter } from "@nestjs/core";
|
|
361
|
+
import { BILLING_ERROR_CODES } from "@saasicat/types";
|
|
362
|
+
function _ts_decorate3(decorators, target, key, desc) {
|
|
363
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
364
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
365
|
+
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;
|
|
366
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
367
|
+
}
|
|
368
|
+
__name(_ts_decorate3, "_ts_decorate");
|
|
369
|
+
var LimitExceededFilter = class _LimitExceededFilter extends BaseExceptionFilter {
|
|
370
|
+
static {
|
|
371
|
+
__name(this, "LimitExceededFilter");
|
|
372
|
+
}
|
|
373
|
+
logger = new Logger2(_LimitExceededFilter.name);
|
|
374
|
+
catch(exception, host) {
|
|
375
|
+
if (!isLimitExceededError(exception)) {
|
|
376
|
+
super.catch(exception, host);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
const ctx = host.switchToHttp();
|
|
380
|
+
const response = ctx.getResponse();
|
|
381
|
+
const request = ctx.getRequest();
|
|
382
|
+
this.logger.warn(`Limit ${exception.dimension} exceeded: ${exception.used}/${exception.max} on ${request.method ?? "?"} ${request.url ?? ""}`);
|
|
383
|
+
response.status(HttpStatus.PAYMENT_REQUIRED).send({
|
|
384
|
+
statusCode: HttpStatus.PAYMENT_REQUIRED,
|
|
385
|
+
error: "PaymentRequired",
|
|
386
|
+
code: BILLING_ERROR_CODES.LIMIT_EXCEEDED,
|
|
387
|
+
// Deprecated: superseded by `code`, kept until consumers have
|
|
388
|
+
// migrated. Same value, only under the older field name.
|
|
389
|
+
reason: BILLING_ERROR_CODES.LIMIT_EXCEEDED,
|
|
390
|
+
dimension: exception.dimension,
|
|
391
|
+
used: exception.used,
|
|
392
|
+
max: exception.max,
|
|
393
|
+
message: exception.message
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
LimitExceededFilter = _ts_decorate3([
|
|
398
|
+
Catch()
|
|
399
|
+
], LimitExceededFilter);
|
|
400
|
+
|
|
401
|
+
// src/billing/plan-catalog-loader.ts
|
|
402
|
+
import { readFileSync } from "fs";
|
|
403
|
+
import { resolve as resolvePath } from "path";
|
|
404
|
+
import yaml from "js-yaml";
|
|
405
|
+
import Ajv2020 from "ajv/dist/2020.js";
|
|
406
|
+
import addFormats from "ajv-formats";
|
|
407
|
+
import { planCatalogSchema } from "@saasicat/spec";
|
|
408
|
+
var PlanCatalogValidationError = class extends Error {
|
|
409
|
+
static {
|
|
410
|
+
__name(this, "PlanCatalogValidationError");
|
|
411
|
+
}
|
|
412
|
+
source;
|
|
413
|
+
errors;
|
|
414
|
+
constructor(source, errors) {
|
|
415
|
+
const messages = errors.map((e) => `${e.instancePath || "/"}: ${e.message ?? "unknown"}`).join("\n ");
|
|
416
|
+
super(`Plan catalog validation failed for ${source}:
|
|
417
|
+
${messages}`), this.source = source, this.errors = errors;
|
|
418
|
+
this.name = "PlanCatalogValidationError";
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
function loadPlanCatalogFromFile(opts) {
|
|
422
|
+
const absolutePath = resolvePath(opts.path);
|
|
423
|
+
const raw = readFileSync(absolutePath, "utf-8");
|
|
424
|
+
return loadPlanCatalogFromString(raw, {
|
|
425
|
+
source: absolutePath,
|
|
426
|
+
crossFieldChecks: opts.crossFieldChecks ?? true
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
__name(loadPlanCatalogFromFile, "loadPlanCatalogFromFile");
|
|
430
|
+
function loadPlanCatalogFromString(yamlContent, opts) {
|
|
431
|
+
const parsed = yaml.load(yamlContent);
|
|
432
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
433
|
+
throw new Error(`The YAML content of ${opts.source} is not an object`);
|
|
434
|
+
}
|
|
435
|
+
const ajv = new Ajv2020({
|
|
436
|
+
strict: false,
|
|
437
|
+
allErrors: true
|
|
438
|
+
});
|
|
439
|
+
addFormats.default(ajv);
|
|
440
|
+
const validate = ajv.compile(planCatalogSchema);
|
|
441
|
+
if (!validate(parsed)) {
|
|
442
|
+
throw new PlanCatalogValidationError(opts.source, validate.errors ?? []);
|
|
443
|
+
}
|
|
444
|
+
const catalog = parsed;
|
|
445
|
+
if (opts.crossFieldChecks ?? true) {
|
|
446
|
+
validateConsistency(catalog, opts.source);
|
|
447
|
+
}
|
|
448
|
+
return catalog;
|
|
449
|
+
}
|
|
450
|
+
__name(loadPlanCatalogFromString, "loadPlanCatalogFromString");
|
|
451
|
+
function validateConsistency(catalog, source) {
|
|
452
|
+
const errors = [];
|
|
453
|
+
const declaredFeatureKeys = new Set((catalog.features ?? []).map((f) => f.key));
|
|
454
|
+
const planIds = /* @__PURE__ */ new Set();
|
|
455
|
+
for (const plan of catalog.plans ?? []) {
|
|
456
|
+
if (planIds.has(plan.id)) {
|
|
457
|
+
errors.push(`plans[].id: duplicate plan id "${plan.id}"`);
|
|
458
|
+
}
|
|
459
|
+
planIds.add(plan.id);
|
|
460
|
+
if (catalog.features) {
|
|
461
|
+
for (const fk of plan.features) {
|
|
462
|
+
if (!declaredFeatureKeys.has(fk)) {
|
|
463
|
+
errors.push(`plans[id=${plan.id}].features: Unknown featureKey "${fk}" \u2014 not declared in catalog.features`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if (errors.length > 0) {
|
|
469
|
+
const err = new Error(`Plan catalog consistency error in ${source}:
|
|
470
|
+
${errors.join("\n ")}`);
|
|
471
|
+
err.name = "PlanCatalogConsistencyError";
|
|
472
|
+
throw err;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
__name(validateConsistency, "validateConsistency");
|
|
476
|
+
|
|
477
|
+
// src/billing/plan-catalog-importer.service.ts
|
|
478
|
+
import { Inject as Inject3, Injectable as Injectable3 } from "@nestjs/common";
|
|
479
|
+
function _ts_decorate4(decorators, target, key, desc) {
|
|
480
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
481
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
482
|
+
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;
|
|
483
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
484
|
+
}
|
|
485
|
+
__name(_ts_decorate4, "_ts_decorate");
|
|
486
|
+
function _ts_metadata3(k, v) {
|
|
487
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
488
|
+
}
|
|
489
|
+
__name(_ts_metadata3, "_ts_metadata");
|
|
490
|
+
function _ts_param3(paramIndex, decorator) {
|
|
491
|
+
return function(target, key) {
|
|
492
|
+
decorator(target, key, paramIndex);
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
__name(_ts_param3, "_ts_param");
|
|
496
|
+
var PLAN_CATALOG_IMPORT_SINK_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/PlanCatalogImportSink");
|
|
497
|
+
var IMPORT_CHANGE_NOTE = "Initial import from saas.yaml";
|
|
498
|
+
var PlanCatalogImporterService = class {
|
|
499
|
+
static {
|
|
500
|
+
__name(this, "PlanCatalogImporterService");
|
|
501
|
+
}
|
|
502
|
+
sink;
|
|
503
|
+
constructor(sink) {
|
|
504
|
+
this.sink = sink;
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Import directly from a YAML string. Apps pass the contents of the
|
|
508
|
+
* `saas.yaml` through via the HTTP body; CLI tools can read the file
|
|
509
|
+
* contents themselves beforehand.
|
|
510
|
+
*
|
|
511
|
+
* @throws on a schema failure of the YAML (validated first via the loader).
|
|
512
|
+
*/
|
|
513
|
+
async importFromYaml(yamlContent, opts = {}) {
|
|
514
|
+
const catalog = loadPlanCatalogFromString(yamlContent, {
|
|
515
|
+
source: opts.source ?? "http-import",
|
|
516
|
+
crossFieldChecks: opts.crossFieldChecks ?? true
|
|
517
|
+
});
|
|
518
|
+
return this.importCatalog(catalog);
|
|
519
|
+
}
|
|
520
|
+
async importCatalog(catalog) {
|
|
521
|
+
const report = {
|
|
522
|
+
plansCreated: 0,
|
|
523
|
+
plansSkipped: 0,
|
|
524
|
+
planVersionsCreated: 0,
|
|
525
|
+
planVersionsSkipped: 0,
|
|
526
|
+
featureEntriesCreated: 0,
|
|
527
|
+
featureEntriesSkipped: 0,
|
|
528
|
+
warnings: []
|
|
529
|
+
};
|
|
530
|
+
for (const feature of catalog.features ?? []) {
|
|
531
|
+
const result = await this.sink.upsertFeatureCatalogEntry({
|
|
532
|
+
projectKey: catalog.projectKey,
|
|
533
|
+
featureKey: feature.key,
|
|
534
|
+
label: feature.label,
|
|
535
|
+
icon: feature.icon,
|
|
536
|
+
tier: feature.tier,
|
|
537
|
+
plannedOnly: feature.plannedOnly
|
|
538
|
+
});
|
|
539
|
+
if (result.created) report.featureEntriesCreated++;
|
|
540
|
+
else report.featureEntriesSkipped++;
|
|
541
|
+
}
|
|
542
|
+
for (const plan of catalog.plans ?? []) {
|
|
543
|
+
const planResult = await this.sink.upsertPlan({
|
|
544
|
+
projectKey: catalog.projectKey,
|
|
545
|
+
planKey: plan.id,
|
|
546
|
+
label: plan.name ?? plan.id,
|
|
547
|
+
description: plan.tagline ?? null
|
|
548
|
+
});
|
|
549
|
+
if (planResult.created) report.plansCreated++;
|
|
550
|
+
else report.plansSkipped++;
|
|
551
|
+
if (plan.monthlyNet === null || plan.monthlyNet === void 0) {
|
|
552
|
+
report.warnings.push(`Plan '${plan.id}' has no monthlyNet \u2014 skipped (on-request plans need a manual plan version).`);
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
const versionResult = await this.sink.upsertPlanVersion({
|
|
556
|
+
planKey: plan.id,
|
|
557
|
+
version: 1,
|
|
558
|
+
features: plan.features,
|
|
559
|
+
quotas: plan.quotas,
|
|
560
|
+
monthlyNet: plan.monthlyNet.toFixed(2),
|
|
561
|
+
yearlyNet: (plan.yearlyNet ?? plan.monthlyNet * 10).toFixed(2),
|
|
562
|
+
marketed: plan.marketed ?? true,
|
|
563
|
+
publish: true,
|
|
564
|
+
changeNote: IMPORT_CHANGE_NOTE
|
|
565
|
+
});
|
|
566
|
+
if (versionResult.created) report.planVersionsCreated++;
|
|
567
|
+
else report.planVersionsSkipped++;
|
|
568
|
+
}
|
|
569
|
+
return report;
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
PlanCatalogImporterService = _ts_decorate4([
|
|
573
|
+
Injectable3(),
|
|
574
|
+
_ts_param3(0, Inject3(PLAN_CATALOG_IMPORT_SINK_TOKEN)),
|
|
575
|
+
_ts_metadata3("design:type", Function),
|
|
576
|
+
_ts_metadata3("design:paramtypes", [
|
|
577
|
+
typeof PlanCatalogImportSink === "undefined" ? Object : PlanCatalogImportSink
|
|
578
|
+
])
|
|
579
|
+
], PlanCatalogImporterService);
|
|
580
|
+
|
|
581
|
+
// src/billing/plan-catalog-importer.module.ts
|
|
582
|
+
import { Module } from "@nestjs/common";
|
|
583
|
+
|
|
584
|
+
// src/billing/plan-catalog-importer.controller.ts
|
|
585
|
+
import { Body, Controller, Inject as Inject4, Post, UseGuards } from "@nestjs/common";
|
|
586
|
+
import { IsBoolean, IsOptional, IsString, MinLength } from "class-validator";
|
|
587
|
+
function _ts_decorate5(decorators, target, key, desc) {
|
|
588
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
589
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
590
|
+
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;
|
|
591
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
592
|
+
}
|
|
593
|
+
__name(_ts_decorate5, "_ts_decorate");
|
|
594
|
+
function _ts_metadata4(k, v) {
|
|
595
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
596
|
+
}
|
|
597
|
+
__name(_ts_metadata4, "_ts_metadata");
|
|
598
|
+
function _ts_param4(paramIndex, decorator) {
|
|
599
|
+
return function(target, key) {
|
|
600
|
+
decorator(target, key, paramIndex);
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
__name(_ts_param4, "_ts_param");
|
|
604
|
+
var PlanCatalogImportDto = class {
|
|
605
|
+
static {
|
|
606
|
+
__name(this, "PlanCatalogImportDto");
|
|
607
|
+
}
|
|
608
|
+
yamlContent;
|
|
609
|
+
crossFieldChecks;
|
|
610
|
+
};
|
|
611
|
+
_ts_decorate5([
|
|
612
|
+
IsString(),
|
|
613
|
+
MinLength(1),
|
|
614
|
+
_ts_metadata4("design:type", String)
|
|
615
|
+
], PlanCatalogImportDto.prototype, "yamlContent", void 0);
|
|
616
|
+
_ts_decorate5([
|
|
617
|
+
IsOptional(),
|
|
618
|
+
IsBoolean(),
|
|
619
|
+
_ts_metadata4("design:type", Boolean)
|
|
620
|
+
], PlanCatalogImportDto.prototype, "crossFieldChecks", void 0);
|
|
621
|
+
function buildPlanCatalogImporterController(guards) {
|
|
622
|
+
let GeneratedPlanCatalogImporterController = class GeneratedPlanCatalogImporterController {
|
|
623
|
+
static {
|
|
624
|
+
__name(this, "GeneratedPlanCatalogImporterController");
|
|
625
|
+
}
|
|
626
|
+
service;
|
|
627
|
+
constructor(service) {
|
|
628
|
+
this.service = service;
|
|
629
|
+
}
|
|
630
|
+
async import(dto) {
|
|
631
|
+
return this.service.importFromYaml(dto.yamlContent, {
|
|
632
|
+
crossFieldChecks: dto.crossFieldChecks
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
_ts_decorate5([
|
|
637
|
+
Post("import"),
|
|
638
|
+
_ts_param4(0, Body()),
|
|
639
|
+
_ts_metadata4("design:type", Function),
|
|
640
|
+
_ts_metadata4("design:paramtypes", [
|
|
641
|
+
typeof PlanCatalogImportDto === "undefined" ? Object : PlanCatalogImportDto
|
|
642
|
+
]),
|
|
643
|
+
_ts_metadata4("design:returntype", Promise)
|
|
644
|
+
], GeneratedPlanCatalogImporterController.prototype, "import", null);
|
|
645
|
+
GeneratedPlanCatalogImporterController = _ts_decorate5([
|
|
646
|
+
Controller("admin/billing/plan-catalog"),
|
|
647
|
+
UseGuards(...guards),
|
|
648
|
+
_ts_param4(0, Inject4(PlanCatalogImporterService)),
|
|
649
|
+
_ts_metadata4("design:type", Function),
|
|
650
|
+
_ts_metadata4("design:paramtypes", [
|
|
651
|
+
typeof PlanCatalogImporterService === "undefined" ? Object : PlanCatalogImporterService
|
|
652
|
+
])
|
|
653
|
+
], GeneratedPlanCatalogImporterController);
|
|
654
|
+
return GeneratedPlanCatalogImporterController;
|
|
655
|
+
}
|
|
656
|
+
__name(buildPlanCatalogImporterController, "buildPlanCatalogImporterController");
|
|
657
|
+
|
|
658
|
+
// src/billing/plan-catalog-importer.module.ts
|
|
659
|
+
function _ts_decorate6(decorators, target, key, desc) {
|
|
660
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
661
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
662
|
+
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;
|
|
663
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
664
|
+
}
|
|
665
|
+
__name(_ts_decorate6, "_ts_decorate");
|
|
666
|
+
var PlanCatalogImporterModule = class _PlanCatalogImporterModule {
|
|
667
|
+
static {
|
|
668
|
+
__name(this, "PlanCatalogImporterModule");
|
|
669
|
+
}
|
|
670
|
+
static forRoot(options) {
|
|
671
|
+
const providers = [
|
|
672
|
+
...options.extraProviders ?? [],
|
|
673
|
+
asProvider(PLAN_CATALOG_IMPORT_SINK_TOKEN, options.sink),
|
|
674
|
+
PlanCatalogImporterService
|
|
675
|
+
];
|
|
676
|
+
const controllers = [];
|
|
677
|
+
if (options.controller) {
|
|
678
|
+
controllers.push(buildPlanCatalogImporterController(options.controller.guards));
|
|
679
|
+
}
|
|
680
|
+
return {
|
|
681
|
+
module: _PlanCatalogImporterModule,
|
|
682
|
+
global: options.global ?? false,
|
|
683
|
+
imports: options.imports ?? [],
|
|
684
|
+
controllers,
|
|
685
|
+
providers,
|
|
686
|
+
exports: [
|
|
687
|
+
PlanCatalogImporterService
|
|
688
|
+
]
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
PlanCatalogImporterModule = _ts_decorate6([
|
|
693
|
+
Module({})
|
|
694
|
+
], PlanCatalogImporterModule);
|
|
695
|
+
|
|
696
|
+
// src/billing/trial-carryover.ts
|
|
697
|
+
var MS_PER_DAY = 864e5;
|
|
698
|
+
function computeCarriedTrialEndsAt(currentTrialDays, newTrialDays, currentTrialEndsAt, now) {
|
|
699
|
+
const daysRemaining = Math.max(0, Math.ceil((currentTrialEndsAt.getTime() - now.getTime()) / MS_PER_DAY));
|
|
700
|
+
const usedDays = Math.max(0, currentTrialDays - daysRemaining);
|
|
701
|
+
const newDays = Math.max(0, newTrialDays - usedDays);
|
|
702
|
+
return new Date(now.getTime() + newDays * MS_PER_DAY);
|
|
703
|
+
}
|
|
704
|
+
__name(computeCarriedTrialEndsAt, "computeCarriedTrialEndsAt");
|
|
705
|
+
|
|
706
|
+
// src/billing/configurator-catalog-builder.ts
|
|
707
|
+
import { Injectable as Injectable4 } from "@nestjs/common";
|
|
708
|
+
function _ts_decorate7(decorators, target, key, desc) {
|
|
709
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
710
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
711
|
+
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;
|
|
712
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
713
|
+
}
|
|
714
|
+
__name(_ts_decorate7, "_ts_decorate");
|
|
715
|
+
var ConfiguratorCatalogBuilder = class {
|
|
716
|
+
static {
|
|
717
|
+
__name(this, "ConfiguratorCatalogBuilder");
|
|
718
|
+
}
|
|
719
|
+
async build(input) {
|
|
720
|
+
const { sources, marketing } = input;
|
|
721
|
+
const planRows = await sources.listLivePlans();
|
|
722
|
+
const planMarketingByPlanId = new Map(marketing.listPlanMarketing().map((m) => [
|
|
723
|
+
m.planId,
|
|
724
|
+
m
|
|
725
|
+
]));
|
|
726
|
+
return {
|
|
727
|
+
cycleDiscount: marketing.getCycleDiscount(),
|
|
728
|
+
currency: marketing.getCurrency(),
|
|
729
|
+
vatRate: marketing.getVatRate(),
|
|
730
|
+
models: planRows.filter((row) => row.marketed).map((row) => buildModel(row, planMarketingByPlanId)).filter((m) => m !== null)
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
ConfiguratorCatalogBuilder = _ts_decorate7([
|
|
735
|
+
Injectable4()
|
|
736
|
+
], ConfiguratorCatalogBuilder);
|
|
737
|
+
function buildModel(row, marketing) {
|
|
738
|
+
const m = marketing.get(row.planId);
|
|
739
|
+
if (!m) return null;
|
|
740
|
+
return {
|
|
741
|
+
id: row.planId.toLowerCase(),
|
|
742
|
+
code: m.code,
|
|
743
|
+
name: m.name,
|
|
744
|
+
glyph: m.glyph,
|
|
745
|
+
tagline: m.tagline,
|
|
746
|
+
planId: row.planId,
|
|
747
|
+
monthlyNet: row.monthlyNet,
|
|
748
|
+
yearlyNet: row.yearlyNet,
|
|
749
|
+
tags: m.tags,
|
|
750
|
+
includedFeatureKeys: row.features,
|
|
751
|
+
quotaBase: normalizeQuotas(row.quotas),
|
|
752
|
+
...m.popular ? {
|
|
753
|
+
popular: true
|
|
754
|
+
} : {}
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
__name(buildModel, "buildModel");
|
|
758
|
+
function normalizeQuotas(quotas) {
|
|
759
|
+
const out = {};
|
|
760
|
+
for (const [k, v] of Object.entries(quotas)) {
|
|
761
|
+
out[k] = v === -1 ? Number.MAX_SAFE_INTEGER : v;
|
|
762
|
+
}
|
|
763
|
+
return out;
|
|
764
|
+
}
|
|
765
|
+
__name(normalizeQuotas, "normalizeQuotas");
|
|
766
|
+
|
|
767
|
+
export {
|
|
768
|
+
PublishValidationError,
|
|
769
|
+
assertChangeNote,
|
|
770
|
+
assertDraftPublishable,
|
|
771
|
+
assertBaseVersionFresh,
|
|
772
|
+
assertOptimisticLockHeld,
|
|
773
|
+
decideRenewal,
|
|
774
|
+
clearPendingPlanVersionFields,
|
|
775
|
+
computeNextPeriod,
|
|
776
|
+
FEATURE_GUARD_CONFIG_TOKEN,
|
|
777
|
+
UPSELL_OFFER_RESOLVER_TOKEN,
|
|
778
|
+
UPSELL_OFFER_CURRENCY_TOKEN,
|
|
779
|
+
FeatureGuard,
|
|
780
|
+
CatalogBundleUpsellResolver,
|
|
781
|
+
LimitExceededFilter,
|
|
782
|
+
PlanCatalogValidationError,
|
|
783
|
+
loadPlanCatalogFromFile,
|
|
784
|
+
loadPlanCatalogFromString,
|
|
785
|
+
PLAN_CATALOG_IMPORT_SINK_TOKEN,
|
|
786
|
+
PlanCatalogImporterService,
|
|
787
|
+
PlanCatalogImportDto,
|
|
788
|
+
buildPlanCatalogImporterController,
|
|
789
|
+
PlanCatalogImporterModule,
|
|
790
|
+
computeCarriedTrialEndsAt,
|
|
791
|
+
ConfiguratorCatalogBuilder
|
|
792
|
+
};
|