@saasicat/nest 0.2.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 +202 -0
- package/README.md +53 -0
- package/dist/admin/index.cjs +1283 -0
- package/dist/admin/index.d.cts +225 -0
- package/dist/admin/index.d.ts +225 -0
- package/dist/admin/index.js +58 -0
- package/dist/admin-audit.service-9IqXMlZm.d.cts +30 -0
- package/dist/admin-audit.service-9IqXMlZm.d.ts +30 -0
- package/dist/admin-manifest.config-DyrQNT7M.d.cts +10 -0
- package/dist/admin-manifest.config-DyrQNT7M.d.ts +10 -0
- package/dist/aggregation-CJ3qQf92.d.cts +288 -0
- package/dist/aggregation-Dvz9e8X2.d.ts +288 -0
- package/dist/billing/index.cjs +5205 -0
- package/dist/billing/index.d.cts +1326 -0
- package/dist/billing/index.d.ts +1326 -0
- package/dist/billing/index.js +197 -0
- package/dist/catalog/index.cjs +6449 -0
- package/dist/catalog/index.d.cts +1073 -0
- package/dist/catalog/index.d.ts +1073 -0
- package/dist/catalog/index.js +158 -0
- package/dist/checkout-offer/index.cjs +807 -0
- package/dist/checkout-offer/index.d.cts +120 -0
- package/dist/checkout-offer/index.d.ts +120 -0
- package/dist/checkout-offer/index.js +20 -0
- package/dist/chunk-2FR6ZL7R.js +0 -0
- package/dist/chunk-2PNX2QL2.js +347 -0
- package/dist/chunk-2VNZYRDV.js +326 -0
- package/dist/chunk-7MLGHWKO.js +12 -0
- package/dist/chunk-ABB2CRAS.js +31 -0
- package/dist/chunk-AJ3EPELS.js +157 -0
- package/dist/chunk-E56W4U2P.js +90 -0
- package/dist/chunk-HRU3CP57.js +71 -0
- package/dist/chunk-L4SUWH5B.js +14 -0
- package/dist/chunk-M6CZQYPY.js +314 -0
- package/dist/chunk-MDIZUVIK.js +87 -0
- package/dist/chunk-MV6LW5UB.js +217 -0
- package/dist/chunk-NYLON2VC.js +6 -0
- package/dist/chunk-P6MYZMXQ.js +951 -0
- package/dist/chunk-Q53N43LQ.js +13 -0
- package/dist/chunk-QJVPRD3R.js +573 -0
- package/dist/chunk-RIGLQ4BV.js +3367 -0
- package/dist/chunk-SABTXESR.js +24 -0
- package/dist/chunk-SHUYVCID.js +6 -0
- package/dist/chunk-SYEVGPFC.js +686 -0
- package/dist/chunk-V7HUZ6UQ.js +5923 -0
- package/dist/chunk-WPUVJRIN.js +29 -0
- package/dist/chunk-XCZ3TTFK.js +22 -0
- package/dist/chunk-XSGU2Y2O.js +1611 -0
- package/dist/chunk-YNEW3L63.js +190 -0
- package/dist/chunk-YXGHPILG.js +614 -0
- package/dist/chunk-ZL4LLE2Z.js +465 -0
- package/dist/di-CcNeq9v-.d.cts +16 -0
- package/dist/di-CcNeq9v-.d.ts +16 -0
- package/dist/discovery/index.cjs +588 -0
- package/dist/discovery/index.d.cts +299 -0
- package/dist/discovery/index.d.ts +299 -0
- package/dist/discovery/index.js +49 -0
- package/dist/discovery.scanner-CUYLKlYT.d.cts +59 -0
- package/dist/discovery.scanner-CUYLKlYT.d.ts +59 -0
- package/dist/entitlement/index.cjs +805 -0
- package/dist/entitlement/index.d.cts +60 -0
- package/dist/entitlement/index.d.ts +60 -0
- package/dist/entitlement/index.js +57 -0
- package/dist/index.cjs +15918 -0
- package/dist/index.d.cts +85 -0
- package/dist/index.d.ts +85 -0
- package/dist/index.js +956 -0
- package/dist/plan-resolution-CFCoUkrE.d.cts +41 -0
- package/dist/plan-resolution-CFCoUkrE.d.ts +41 -0
- package/dist/platform/index.cjs +2772 -0
- package/dist/platform/index.d.cts +118 -0
- package/dist/platform/index.d.ts +118 -0
- package/dist/platform/index.js +37 -0
- package/dist/promo/index.cjs +995 -0
- package/dist/promo/index.d.cts +121 -0
- package/dist/promo/index.d.ts +121 -0
- package/dist/promo/index.js +58 -0
- package/dist/registration/index.cjs +1687 -0
- package/dist/registration/index.d.cts +444 -0
- package/dist/registration/index.d.ts +444 -0
- package/dist/registration/index.js +82 -0
- package/dist/saas-platform.module-CgZ2omgg.d.cts +180 -0
- package/dist/saas-platform.module-DT9TnCzk.d.ts +180 -0
- package/dist/service-DX8KbGXl.d.cts +109 -0
- package/dist/service-DX8KbGXl.d.ts +109 -0
- package/dist/subscription-contract/index.cjs +471 -0
- package/dist/subscription-contract/index.d.cts +17 -0
- package/dist/subscription-contract/index.d.ts +17 -0
- package/dist/subscription-contract/index.js +21 -0
- package/dist/subscription-contract.service--cm47ZJJ.d.cts +39 -0
- package/dist/subscription-contract.service--cm47ZJJ.d.ts +39 -0
- package/dist/testing/index.cjs +3942 -0
- package/dist/testing/index.d.cts +279 -0
- package/dist/testing/index.d.ts +279 -0
- package/dist/testing/index.js +1195 -0
- package/package.json +185 -0
|
@@ -0,0 +1,1195 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SaasPlatformModule
|
|
3
|
+
} from "../chunk-QJVPRD3R.js";
|
|
4
|
+
import "../chunk-P6MYZMXQ.js";
|
|
5
|
+
import "../chunk-Q53N43LQ.js";
|
|
6
|
+
import "../chunk-E56W4U2P.js";
|
|
7
|
+
import "../chunk-AJ3EPELS.js";
|
|
8
|
+
import "../chunk-M6CZQYPY.js";
|
|
9
|
+
import "../chunk-YXGHPILG.js";
|
|
10
|
+
import "../chunk-MV6LW5UB.js";
|
|
11
|
+
import "../chunk-NYLON2VC.js";
|
|
12
|
+
import "../chunk-7MLGHWKO.js";
|
|
13
|
+
import "../chunk-XCZ3TTFK.js";
|
|
14
|
+
import "../chunk-SABTXESR.js";
|
|
15
|
+
import "../chunk-L4SUWH5B.js";
|
|
16
|
+
import {
|
|
17
|
+
__name
|
|
18
|
+
} from "../chunk-SHUYVCID.js";
|
|
19
|
+
|
|
20
|
+
// src/testing/fake-repositories.ts
|
|
21
|
+
import { startOfUtcDay } from "@saasicat/types";
|
|
22
|
+
var FakeSubscriptionRepository = class {
|
|
23
|
+
static {
|
|
24
|
+
__name(this, "FakeSubscriptionRepository");
|
|
25
|
+
}
|
|
26
|
+
byTenantId = /* @__PURE__ */ new Map();
|
|
27
|
+
/**
|
|
28
|
+
* Test stubs for the `countBy*` editability methods. Tests set them
|
|
29
|
+
* directly; default 0 (= "no subscription binds the version" →
|
|
30
|
+
* published-but-future versions are editable when they are latest-in-chain
|
|
31
|
+
* and validFrom lies in the future).
|
|
32
|
+
*/
|
|
33
|
+
planVersionCounts = /* @__PURE__ */ new Map();
|
|
34
|
+
bundleVersionCounts = /* @__PURE__ */ new Map();
|
|
35
|
+
set(record) {
|
|
36
|
+
this.byTenantId.set(record.tenantId, record);
|
|
37
|
+
}
|
|
38
|
+
delete(tenantId) {
|
|
39
|
+
this.byTenantId.delete(tenantId);
|
|
40
|
+
}
|
|
41
|
+
clear() {
|
|
42
|
+
this.byTenantId.clear();
|
|
43
|
+
this.planVersionCounts.clear();
|
|
44
|
+
this.bundleVersionCounts.clear();
|
|
45
|
+
}
|
|
46
|
+
/** Test helper: force the number of subscriptions for a PlanVersion. */
|
|
47
|
+
setPlanVersionCount(planVersionId, count) {
|
|
48
|
+
this.planVersionCounts.set(planVersionId, count);
|
|
49
|
+
}
|
|
50
|
+
/** Test helper: force the number of subscriptions for a BundleVersion. */
|
|
51
|
+
setBundleVersionCount(bundleVersionId, count) {
|
|
52
|
+
this.bundleVersionCounts.set(bundleVersionId, count);
|
|
53
|
+
}
|
|
54
|
+
async findByTenantId(tenantId) {
|
|
55
|
+
return this.byTenantId.get(tenantId) ?? null;
|
|
56
|
+
}
|
|
57
|
+
async findByTenantIdLocked(tenantId, _tx) {
|
|
58
|
+
return this.findByTenantId(tenantId);
|
|
59
|
+
}
|
|
60
|
+
async countByPlanVersionId(planVersionId) {
|
|
61
|
+
return this.planVersionCounts.get(planVersionId) ?? 0;
|
|
62
|
+
}
|
|
63
|
+
async countByBundleVersionId(bundleVersionId) {
|
|
64
|
+
return this.bundleVersionCounts.get(bundleVersionId) ?? 0;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
var FakeSubscriptionBundleRepository = class {
|
|
68
|
+
static {
|
|
69
|
+
__name(this, "FakeSubscriptionBundleRepository");
|
|
70
|
+
}
|
|
71
|
+
byId = /* @__PURE__ */ new Map();
|
|
72
|
+
nextId = 1;
|
|
73
|
+
now() {
|
|
74
|
+
return /* @__PURE__ */ new Date("2026-01-01T00:00:00.000Z");
|
|
75
|
+
}
|
|
76
|
+
clear() {
|
|
77
|
+
this.byId.clear();
|
|
78
|
+
}
|
|
79
|
+
async listBySubscription(subscriptionId) {
|
|
80
|
+
return [
|
|
81
|
+
...this.byId.values()
|
|
82
|
+
].filter((r) => r.subscriptionId === subscriptionId).sort((a, b) => b.startedAt.getTime() - a.startedAt.getTime());
|
|
83
|
+
}
|
|
84
|
+
async findById(subscriptionBundleId) {
|
|
85
|
+
return this.byId.get(subscriptionBundleId) ?? null;
|
|
86
|
+
}
|
|
87
|
+
async listActiveBySubscription(subscriptionId, asOf = /* @__PURE__ */ new Date()) {
|
|
88
|
+
return [
|
|
89
|
+
...this.byId.values()
|
|
90
|
+
].filter((r) => r.subscriptionId === subscriptionId && (r.canceledAt === null || r.canceledEffectiveAt !== null && r.canceledEffectiveAt.getTime() > asOf.getTime()));
|
|
91
|
+
}
|
|
92
|
+
async add(data) {
|
|
93
|
+
const id = `sub-bundle-${this.nextId++}`;
|
|
94
|
+
const createdAt = this.now();
|
|
95
|
+
const row = {
|
|
96
|
+
id,
|
|
97
|
+
subscriptionId: data.subscriptionId,
|
|
98
|
+
bundleVersionId: data.bundleVersionId,
|
|
99
|
+
startedAt: data.startedAt,
|
|
100
|
+
minimumTermEndsAt: data.minimumTermEndsAt ?? null,
|
|
101
|
+
canceledAt: null,
|
|
102
|
+
canceledEffectiveAt: null,
|
|
103
|
+
createdAt,
|
|
104
|
+
updatedAt: createdAt
|
|
105
|
+
};
|
|
106
|
+
this.byId.set(id, row);
|
|
107
|
+
return row;
|
|
108
|
+
}
|
|
109
|
+
async cancel(subscriptionBundleId, data) {
|
|
110
|
+
const existing = this.byId.get(subscriptionBundleId);
|
|
111
|
+
if (!existing) {
|
|
112
|
+
throw new Error(`SubscriptionBundle '${subscriptionBundleId}' nicht gefunden`);
|
|
113
|
+
}
|
|
114
|
+
if (existing.canceledAt !== null) {
|
|
115
|
+
throw new Error(`SubscriptionBundle '${subscriptionBundleId}' ist bereits gek\xFCndigt`);
|
|
116
|
+
}
|
|
117
|
+
const updated = {
|
|
118
|
+
...existing,
|
|
119
|
+
canceledAt: data.canceledAt,
|
|
120
|
+
canceledEffectiveAt: data.canceledEffectiveAt,
|
|
121
|
+
updatedAt: this.now()
|
|
122
|
+
};
|
|
123
|
+
this.byId.set(subscriptionBundleId, updated);
|
|
124
|
+
return updated;
|
|
125
|
+
}
|
|
126
|
+
async reactivate(subscriptionBundleId) {
|
|
127
|
+
const existing = this.byId.get(subscriptionBundleId);
|
|
128
|
+
if (!existing) {
|
|
129
|
+
throw new Error(`SubscriptionBundle '${subscriptionBundleId}' nicht gefunden`);
|
|
130
|
+
}
|
|
131
|
+
const updated = {
|
|
132
|
+
...existing,
|
|
133
|
+
canceledAt: null,
|
|
134
|
+
canceledEffectiveAt: null,
|
|
135
|
+
updatedAt: this.now()
|
|
136
|
+
};
|
|
137
|
+
this.byId.set(subscriptionBundleId, updated);
|
|
138
|
+
return updated;
|
|
139
|
+
}
|
|
140
|
+
async countActiveByBundleVersionId(bundleVersionId, asOf = /* @__PURE__ */ new Date()) {
|
|
141
|
+
let n = 0;
|
|
142
|
+
for (const r of this.byId.values()) {
|
|
143
|
+
if (r.bundleVersionId !== bundleVersionId) continue;
|
|
144
|
+
if (r.canceledAt === null) n += 1;
|
|
145
|
+
else if (r.canceledEffectiveAt !== null && r.canceledEffectiveAt.getTime() > asOf.getTime()) {
|
|
146
|
+
n += 1;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return n;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
var FakeSubscriptionContractRepository = class {
|
|
153
|
+
static {
|
|
154
|
+
__name(this, "FakeSubscriptionContractRepository");
|
|
155
|
+
}
|
|
156
|
+
byId = /* @__PURE__ */ new Map();
|
|
157
|
+
nextContractId = 1;
|
|
158
|
+
nextLineItemId = 1;
|
|
159
|
+
clear() {
|
|
160
|
+
this.byId.clear();
|
|
161
|
+
this.nextContractId = 1;
|
|
162
|
+
this.nextLineItemId = 1;
|
|
163
|
+
}
|
|
164
|
+
async list(filter) {
|
|
165
|
+
return [
|
|
166
|
+
...this.byId.values()
|
|
167
|
+
].filter((row) => !filter.projectKey || row.projectKey === filter.projectKey).filter((row) => !filter.tenantId || row.tenantId === filter.tenantId).filter((row) => !filter.status || row.status === filter.status).filter((row) => !filter.asOf || this.isActiveAt(row, filter.asOf)).sort((a, b) => b.effectiveFrom.getTime() - a.effectiveFrom.getTime()).map((row) => this.cloneRecord(row));
|
|
168
|
+
}
|
|
169
|
+
async findById(contractId) {
|
|
170
|
+
const row = this.byId.get(contractId);
|
|
171
|
+
return row ? this.cloneRecord(row) : null;
|
|
172
|
+
}
|
|
173
|
+
async findActiveByTenantId(tenantId, asOf = /* @__PURE__ */ new Date()) {
|
|
174
|
+
const active = [
|
|
175
|
+
...this.byId.values()
|
|
176
|
+
].filter((row) => row.tenantId === tenantId).filter((row) => this.isActiveAt(row, asOf)).sort((a, b) => b.effectiveFrom.getTime() - a.effectiveFrom.getTime())[0];
|
|
177
|
+
return active ? this.cloneRecord(active) : null;
|
|
178
|
+
}
|
|
179
|
+
async create(data) {
|
|
180
|
+
const now = /* @__PURE__ */ new Date();
|
|
181
|
+
const id = `contract-${this.nextContractId++}`;
|
|
182
|
+
const row = {
|
|
183
|
+
id,
|
|
184
|
+
projectKey: data.projectKey,
|
|
185
|
+
tenantId: data.tenantId,
|
|
186
|
+
status: data.status ?? "active",
|
|
187
|
+
effectiveFrom: new Date(data.effectiveFrom),
|
|
188
|
+
effectiveUntil: data.effectiveUntil ? new Date(data.effectiveUntil) : null,
|
|
189
|
+
originalOfferId: data.originalOfferId ?? null,
|
|
190
|
+
originalPlanVersionId: data.originalPlanVersionId ?? null,
|
|
191
|
+
originalBundleVersionIds: [
|
|
192
|
+
...data.originalBundleVersionIds ?? []
|
|
193
|
+
],
|
|
194
|
+
entitlementSnapshot: data.entitlementSnapshot ? {
|
|
195
|
+
plan: data.entitlementSnapshot.plan,
|
|
196
|
+
quotas: {
|
|
197
|
+
...data.entitlementSnapshot.quotas
|
|
198
|
+
},
|
|
199
|
+
features: [
|
|
200
|
+
...data.entitlementSnapshot.features
|
|
201
|
+
]
|
|
202
|
+
} : null,
|
|
203
|
+
priceSnapshot: {
|
|
204
|
+
...data.priceSnapshot
|
|
205
|
+
},
|
|
206
|
+
promotionSnapshots: [
|
|
207
|
+
...data.promotionSnapshots ?? []
|
|
208
|
+
],
|
|
209
|
+
promoCodeSnapshots: [
|
|
210
|
+
...data.promoCodeSnapshots ?? []
|
|
211
|
+
],
|
|
212
|
+
termsSnapshot: data.termsSnapshot ? {
|
|
213
|
+
...data.termsSnapshot
|
|
214
|
+
} : null,
|
|
215
|
+
lineItems: data.lineItems.map((item) => ({
|
|
216
|
+
...item,
|
|
217
|
+
id: `contract-line-${this.nextLineItemId++}`,
|
|
218
|
+
contractId: id,
|
|
219
|
+
minimumTermUntil: item.minimumTermUntil ? new Date(item.minimumTermUntil) : null,
|
|
220
|
+
featuresSnapshot: [
|
|
221
|
+
...item.featuresSnapshot
|
|
222
|
+
],
|
|
223
|
+
quotaEffectsSnapshot: {
|
|
224
|
+
...item.quotaEffectsSnapshot
|
|
225
|
+
},
|
|
226
|
+
metadata: item.metadata ? {
|
|
227
|
+
...item.metadata
|
|
228
|
+
} : null,
|
|
229
|
+
createdAt: now
|
|
230
|
+
})),
|
|
231
|
+
createdAt: now,
|
|
232
|
+
updatedAt: now
|
|
233
|
+
};
|
|
234
|
+
this.byId.set(id, row);
|
|
235
|
+
return this.cloneRecord(row);
|
|
236
|
+
}
|
|
237
|
+
async terminate(contractId, data) {
|
|
238
|
+
const existing = this.byId.get(contractId);
|
|
239
|
+
if (!existing) throw new Error(`SubscriptionContract '${contractId}' nicht gefunden`);
|
|
240
|
+
const updated = {
|
|
241
|
+
...existing,
|
|
242
|
+
status: data.status,
|
|
243
|
+
effectiveUntil: new Date(data.effectiveUntil),
|
|
244
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
245
|
+
};
|
|
246
|
+
this.byId.set(contractId, updated);
|
|
247
|
+
return this.cloneRecord(updated);
|
|
248
|
+
}
|
|
249
|
+
isActiveAt(row, asOf) {
|
|
250
|
+
if (row.status !== "active" && row.status !== "scheduled") return false;
|
|
251
|
+
return row.effectiveFrom.getTime() <= asOf.getTime() && (row.effectiveUntil === null || row.effectiveUntil.getTime() > asOf.getTime());
|
|
252
|
+
}
|
|
253
|
+
cloneRecord(row) {
|
|
254
|
+
return {
|
|
255
|
+
...row,
|
|
256
|
+
effectiveFrom: new Date(row.effectiveFrom),
|
|
257
|
+
effectiveUntil: row.effectiveUntil ? new Date(row.effectiveUntil) : null,
|
|
258
|
+
originalBundleVersionIds: [
|
|
259
|
+
...row.originalBundleVersionIds
|
|
260
|
+
],
|
|
261
|
+
entitlementSnapshot: row.entitlementSnapshot ? {
|
|
262
|
+
plan: row.entitlementSnapshot.plan,
|
|
263
|
+
quotas: {
|
|
264
|
+
...row.entitlementSnapshot.quotas
|
|
265
|
+
},
|
|
266
|
+
features: [
|
|
267
|
+
...row.entitlementSnapshot.features
|
|
268
|
+
]
|
|
269
|
+
} : null,
|
|
270
|
+
priceSnapshot: {
|
|
271
|
+
...row.priceSnapshot
|
|
272
|
+
},
|
|
273
|
+
promotionSnapshots: [
|
|
274
|
+
...row.promotionSnapshots
|
|
275
|
+
],
|
|
276
|
+
promoCodeSnapshots: [
|
|
277
|
+
...row.promoCodeSnapshots
|
|
278
|
+
],
|
|
279
|
+
termsSnapshot: row.termsSnapshot ? {
|
|
280
|
+
...row.termsSnapshot
|
|
281
|
+
} : null,
|
|
282
|
+
lineItems: row.lineItems.map((item) => ({
|
|
283
|
+
...item,
|
|
284
|
+
minimumTermUntil: item.minimumTermUntil ? new Date(item.minimumTermUntil) : null,
|
|
285
|
+
featuresSnapshot: [
|
|
286
|
+
...item.featuresSnapshot
|
|
287
|
+
],
|
|
288
|
+
quotaEffectsSnapshot: {
|
|
289
|
+
...item.quotaEffectsSnapshot
|
|
290
|
+
},
|
|
291
|
+
metadata: item.metadata ? {
|
|
292
|
+
...item.metadata
|
|
293
|
+
} : null,
|
|
294
|
+
createdAt: new Date(item.createdAt)
|
|
295
|
+
})),
|
|
296
|
+
createdAt: new Date(row.createdAt),
|
|
297
|
+
updatedAt: new Date(row.updatedAt)
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
var FakePlanVersionRepository = class {
|
|
302
|
+
static {
|
|
303
|
+
__name(this, "FakePlanVersionRepository");
|
|
304
|
+
}
|
|
305
|
+
byPlanId = /* @__PURE__ */ new Map();
|
|
306
|
+
set(record) {
|
|
307
|
+
this.byPlanId.set(record.planId, record);
|
|
308
|
+
}
|
|
309
|
+
clear() {
|
|
310
|
+
this.byPlanId.clear();
|
|
311
|
+
}
|
|
312
|
+
async findLatestLive(planId, _tx) {
|
|
313
|
+
return this.byPlanId.get(planId) ?? null;
|
|
314
|
+
}
|
|
315
|
+
// The fake keeps only one record per planId — `findActive` returns the
|
|
316
|
+
// same as `findLatestLive`. Consumers test real time-window semantics
|
|
317
|
+
// against the Prisma implementation or against `FakePlanRepository`
|
|
318
|
+
// (which supports multiple versions per planKey).
|
|
319
|
+
async findActive(planId, _asOf, _tx) {
|
|
320
|
+
return this.byPlanId.get(planId) ?? null;
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
var FakeTransactionRunner = class _FakeTransactionRunner {
|
|
324
|
+
static {
|
|
325
|
+
__name(this, "FakeTransactionRunner");
|
|
326
|
+
}
|
|
327
|
+
/** Sentinel value passed through as the TransactionContext. */
|
|
328
|
+
static TX_SENTINEL = /* @__PURE__ */ Symbol.for("FakeTransactionRunner.tx");
|
|
329
|
+
runCount = 0;
|
|
330
|
+
async run(fn) {
|
|
331
|
+
this.runCount += 1;
|
|
332
|
+
return fn(_FakeTransactionRunner.TX_SENTINEL);
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
var FakeBundleRepository = class {
|
|
336
|
+
static {
|
|
337
|
+
__name(this, "FakeBundleRepository");
|
|
338
|
+
}
|
|
339
|
+
bundles = /* @__PURE__ */ new Map();
|
|
340
|
+
versions = /* @__PURE__ */ new Map();
|
|
341
|
+
nextId = 1;
|
|
342
|
+
genId(prefix) {
|
|
343
|
+
const n = this.nextId++;
|
|
344
|
+
const suffix = n.toString(16).padStart(12, "0");
|
|
345
|
+
return `${prefix}aaaa-aaaa-aaaa-aaaa-${suffix}`;
|
|
346
|
+
}
|
|
347
|
+
nowIso() {
|
|
348
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
349
|
+
}
|
|
350
|
+
// Test helpers
|
|
351
|
+
seedBundle(row) {
|
|
352
|
+
this.bundles.set(row.id, row);
|
|
353
|
+
}
|
|
354
|
+
seedVersion(row) {
|
|
355
|
+
this.versions.set(row.id, row);
|
|
356
|
+
}
|
|
357
|
+
clear() {
|
|
358
|
+
this.bundles.clear();
|
|
359
|
+
this.versions.clear();
|
|
360
|
+
}
|
|
361
|
+
// ─── Master operations ───
|
|
362
|
+
async list(filter) {
|
|
363
|
+
const excludeDeleted = filter.excludeDeleted ?? true;
|
|
364
|
+
return [
|
|
365
|
+
...this.bundles.values()
|
|
366
|
+
].filter((b) => b.projectKey === filter.projectKey && (!excludeDeleted || b.deletedAt === null));
|
|
367
|
+
}
|
|
368
|
+
async findById(bundleId) {
|
|
369
|
+
return this.bundles.get(bundleId) ?? null;
|
|
370
|
+
}
|
|
371
|
+
async findByKey(projectKey, bundleKey) {
|
|
372
|
+
for (const b of this.bundles.values()) {
|
|
373
|
+
if (b.projectKey === projectKey && b.bundleKey === bundleKey && b.deletedAt === null) {
|
|
374
|
+
return b;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
async create(data) {
|
|
380
|
+
const now = this.nowIso();
|
|
381
|
+
const row = {
|
|
382
|
+
id: this.genId("bbbb"),
|
|
383
|
+
projectKey: data.projectKey,
|
|
384
|
+
bundleKey: data.bundleKey,
|
|
385
|
+
label: data.label,
|
|
386
|
+
description: data.description ?? null,
|
|
387
|
+
icon: data.icon ?? null,
|
|
388
|
+
sortOrder: data.sortOrder ?? 0,
|
|
389
|
+
i18n: data.i18n ?? {},
|
|
390
|
+
createdAt: now,
|
|
391
|
+
updatedAt: now,
|
|
392
|
+
deletedAt: null
|
|
393
|
+
};
|
|
394
|
+
this.bundles.set(row.id, row);
|
|
395
|
+
return row;
|
|
396
|
+
}
|
|
397
|
+
async update(bundleId, data) {
|
|
398
|
+
const existing = this.bundles.get(bundleId);
|
|
399
|
+
if (!existing) throw new Error(`Bundle '${bundleId}' nicht gefunden`);
|
|
400
|
+
const updated = {
|
|
401
|
+
...existing,
|
|
402
|
+
label: data.label ?? existing.label,
|
|
403
|
+
description: data.description !== void 0 ? data.description : existing.description,
|
|
404
|
+
icon: data.icon !== void 0 ? data.icon : existing.icon,
|
|
405
|
+
sortOrder: data.sortOrder ?? existing.sortOrder,
|
|
406
|
+
i18n: data.i18n ?? existing.i18n,
|
|
407
|
+
updatedAt: this.nowIso()
|
|
408
|
+
};
|
|
409
|
+
this.bundles.set(bundleId, updated);
|
|
410
|
+
return updated;
|
|
411
|
+
}
|
|
412
|
+
async softDelete(bundleId) {
|
|
413
|
+
const existing = this.bundles.get(bundleId);
|
|
414
|
+
if (!existing) throw new Error(`Bundle '${bundleId}' nicht gefunden`);
|
|
415
|
+
this.bundles.set(bundleId, {
|
|
416
|
+
...existing,
|
|
417
|
+
deletedAt: this.nowIso()
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
// ─── Version operations ───
|
|
421
|
+
async listVersions(bundleId) {
|
|
422
|
+
return [
|
|
423
|
+
...this.versions.values()
|
|
424
|
+
].filter((v) => v.bundleId === bundleId).sort((a, b) => a.version - b.version);
|
|
425
|
+
}
|
|
426
|
+
async findVersionById(versionId) {
|
|
427
|
+
return this.versions.get(versionId) ?? null;
|
|
428
|
+
}
|
|
429
|
+
async findCurrentDraft(bundleId) {
|
|
430
|
+
for (const v of this.versions.values()) {
|
|
431
|
+
if (v.bundleId === bundleId && v.publishedAt === null) return v;
|
|
432
|
+
}
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
async findLatestLive(bundleId) {
|
|
436
|
+
for (const v of this.versions.values()) {
|
|
437
|
+
if (v.bundleId === bundleId && v.publishedAt !== null && v.supersededAt === null) {
|
|
438
|
+
return v;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
async createDraft(data) {
|
|
444
|
+
const draft = await this.findCurrentDraft(data.bundleId);
|
|
445
|
+
if (draft) {
|
|
446
|
+
throw new Error(`Bundle '${data.bundleId}' hat bereits eine Draft-Version v${draft.version}`);
|
|
447
|
+
}
|
|
448
|
+
const all = await this.listVersions(data.bundleId);
|
|
449
|
+
const nextVersion = all.length === 0 ? 1 : Math.max(...all.map((v) => v.version)) + 1;
|
|
450
|
+
const now = this.nowIso();
|
|
451
|
+
const row = {
|
|
452
|
+
id: this.genId("vvvv"),
|
|
453
|
+
version: nextVersion,
|
|
454
|
+
baseVersionId: data.baseVersionId ?? null,
|
|
455
|
+
publishedAt: null,
|
|
456
|
+
supersededAt: null,
|
|
457
|
+
publishedChanges: null,
|
|
458
|
+
changeNote: data.changeNote ?? "",
|
|
459
|
+
nonRegressive: true,
|
|
460
|
+
createdByUserId: data.createdByUserId ?? null,
|
|
461
|
+
publishedByUserId: null,
|
|
462
|
+
validFrom: data.validFrom ?? null,
|
|
463
|
+
validUntil: data.validUntil ?? null,
|
|
464
|
+
createdAt: now,
|
|
465
|
+
updatedAt: now,
|
|
466
|
+
bundleId: data.bundleId,
|
|
467
|
+
bundleKey: this.bundles.get(data.bundleId)?.bundleKey ?? "",
|
|
468
|
+
label: this.bundles.get(data.bundleId)?.label ?? "",
|
|
469
|
+
features: data.features,
|
|
470
|
+
quotas: data.quotas ?? {},
|
|
471
|
+
compatibility: data.compatibility ?? {},
|
|
472
|
+
pricingOverrides: data.pricingOverrides ?? [],
|
|
473
|
+
monthlyNet: data.monthlyNet ?? null,
|
|
474
|
+
yearlyNet: data.yearlyNet ?? null,
|
|
475
|
+
marketed: data.marketed ?? true
|
|
476
|
+
};
|
|
477
|
+
this.versions.set(row.id, row);
|
|
478
|
+
return row;
|
|
479
|
+
}
|
|
480
|
+
async updateDraft(versionId, data) {
|
|
481
|
+
const existing = this.versions.get(versionId);
|
|
482
|
+
if (!existing) throw new Error(`BundleVersion '${versionId}' nicht gefunden`);
|
|
483
|
+
const updated = {
|
|
484
|
+
...existing,
|
|
485
|
+
features: data.features ?? existing.features,
|
|
486
|
+
quotas: data.quotas ?? existing.quotas,
|
|
487
|
+
compatibility: data.compatibility ?? existing.compatibility,
|
|
488
|
+
pricingOverrides: data.pricingOverrides ?? existing.pricingOverrides,
|
|
489
|
+
monthlyNet: data.monthlyNet !== void 0 ? data.monthlyNet : existing.monthlyNet,
|
|
490
|
+
yearlyNet: data.yearlyNet !== void 0 ? data.yearlyNet : existing.yearlyNet,
|
|
491
|
+
marketed: data.marketed ?? existing.marketed,
|
|
492
|
+
changeNote: data.changeNote ?? existing.changeNote,
|
|
493
|
+
validFrom: data.validFrom !== void 0 ? data.validFrom : existing.validFrom,
|
|
494
|
+
validUntil: data.validUntil !== void 0 ? data.validUntil : existing.validUntil,
|
|
495
|
+
updatedAt: this.nowIso()
|
|
496
|
+
};
|
|
497
|
+
this.versions.set(versionId, updated);
|
|
498
|
+
return updated;
|
|
499
|
+
}
|
|
500
|
+
async deleteDraft(versionId) {
|
|
501
|
+
const existing = this.versions.get(versionId);
|
|
502
|
+
if (!existing) return;
|
|
503
|
+
if (existing.publishedAt !== null) {
|
|
504
|
+
throw new Error(`BundleVersion '${versionId}' ist bereits published`);
|
|
505
|
+
}
|
|
506
|
+
this.versions.delete(versionId);
|
|
507
|
+
}
|
|
508
|
+
async publishDraft(versionId, publishMeta) {
|
|
509
|
+
const draft = this.versions.get(versionId);
|
|
510
|
+
if (!draft) throw new Error(`BundleVersion '${versionId}' nicht gefunden`);
|
|
511
|
+
if (draft.publishedAt !== null) {
|
|
512
|
+
throw new Error(`BundleVersion '${versionId}' ist bereits published`);
|
|
513
|
+
}
|
|
514
|
+
const now = this.nowIso();
|
|
515
|
+
const validFromIso = publishMeta.validFrom.toISOString();
|
|
516
|
+
const validUntilIso = publishMeta.validUntil ? publishMeta.validUntil.toISOString() : null;
|
|
517
|
+
const previous = await this.findLatestLive(draft.bundleId);
|
|
518
|
+
if (previous) {
|
|
519
|
+
const dayMs = 24 * 60 * 60 * 1e3;
|
|
520
|
+
const inheritedValidUntil = new Date(publishMeta.validFrom.getTime() - dayMs).toISOString();
|
|
521
|
+
this.versions.set(previous.id, {
|
|
522
|
+
...previous,
|
|
523
|
+
supersededAt: now,
|
|
524
|
+
validUntil: inheritedValidUntil,
|
|
525
|
+
updatedAt: now
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
const published = {
|
|
529
|
+
...draft,
|
|
530
|
+
publishedAt: now,
|
|
531
|
+
publishedChanges: publishMeta.publishedChanges,
|
|
532
|
+
nonRegressive: publishMeta.nonRegressive,
|
|
533
|
+
publishedByUserId: publishMeta.publishedByUserId,
|
|
534
|
+
validFrom: validFromIso,
|
|
535
|
+
validUntil: validUntilIso,
|
|
536
|
+
updatedAt: now
|
|
537
|
+
};
|
|
538
|
+
this.versions.set(versionId, published);
|
|
539
|
+
return published;
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
var FakeBusinessTypeRepository = class {
|
|
543
|
+
static {
|
|
544
|
+
__name(this, "FakeBusinessTypeRepository");
|
|
545
|
+
}
|
|
546
|
+
types = /* @__PURE__ */ new Map();
|
|
547
|
+
versions = /* @__PURE__ */ new Map();
|
|
548
|
+
nextId = 1;
|
|
549
|
+
genId(prefix) {
|
|
550
|
+
const n = this.nextId++;
|
|
551
|
+
const suffix = n.toString(16).padStart(12, "0");
|
|
552
|
+
return `${prefix}aaaa-aaaa-aaaa-aaaa-${suffix}`;
|
|
553
|
+
}
|
|
554
|
+
nowIso() {
|
|
555
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
556
|
+
}
|
|
557
|
+
seedBusinessType(row) {
|
|
558
|
+
this.types.set(row.id, row);
|
|
559
|
+
}
|
|
560
|
+
seedVersion(row) {
|
|
561
|
+
this.versions.set(row.id, row);
|
|
562
|
+
}
|
|
563
|
+
clear() {
|
|
564
|
+
this.types.clear();
|
|
565
|
+
this.versions.clear();
|
|
566
|
+
}
|
|
567
|
+
async list(filter) {
|
|
568
|
+
const excludeDeleted = filter.excludeDeleted ?? true;
|
|
569
|
+
return [
|
|
570
|
+
...this.types.values()
|
|
571
|
+
].filter((t) => t.projectKey === filter.projectKey && (!excludeDeleted || t.deletedAt === null));
|
|
572
|
+
}
|
|
573
|
+
async findById(businessTypeId) {
|
|
574
|
+
return this.types.get(businessTypeId) ?? null;
|
|
575
|
+
}
|
|
576
|
+
async findByKey(projectKey, businessTypeKey) {
|
|
577
|
+
for (const t of this.types.values()) {
|
|
578
|
+
if (t.projectKey === projectKey && t.businessTypeKey === businessTypeKey && t.deletedAt === null) {
|
|
579
|
+
return t;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
584
|
+
async create(data) {
|
|
585
|
+
const now = this.nowIso();
|
|
586
|
+
const row = {
|
|
587
|
+
id: this.genId("cccc"),
|
|
588
|
+
projectKey: data.projectKey,
|
|
589
|
+
businessTypeKey: data.businessTypeKey,
|
|
590
|
+
label: data.label,
|
|
591
|
+
description: data.description ?? null,
|
|
592
|
+
icon: data.icon ?? null,
|
|
593
|
+
sortOrder: data.sortOrder ?? 0,
|
|
594
|
+
createdAt: now,
|
|
595
|
+
updatedAt: now,
|
|
596
|
+
deletedAt: null
|
|
597
|
+
};
|
|
598
|
+
this.types.set(row.id, row);
|
|
599
|
+
return row;
|
|
600
|
+
}
|
|
601
|
+
async update(businessTypeId, data) {
|
|
602
|
+
const existing = this.types.get(businessTypeId);
|
|
603
|
+
if (!existing) throw new Error(`BusinessType '${businessTypeId}' nicht gefunden`);
|
|
604
|
+
const updated = {
|
|
605
|
+
...existing,
|
|
606
|
+
label: data.label ?? existing.label,
|
|
607
|
+
description: data.description !== void 0 ? data.description : existing.description,
|
|
608
|
+
icon: data.icon !== void 0 ? data.icon : existing.icon,
|
|
609
|
+
sortOrder: data.sortOrder ?? existing.sortOrder,
|
|
610
|
+
updatedAt: this.nowIso()
|
|
611
|
+
};
|
|
612
|
+
this.types.set(businessTypeId, updated);
|
|
613
|
+
return updated;
|
|
614
|
+
}
|
|
615
|
+
async softDelete(businessTypeId) {
|
|
616
|
+
const existing = this.types.get(businessTypeId);
|
|
617
|
+
if (!existing) throw new Error(`BusinessType '${businessTypeId}' nicht gefunden`);
|
|
618
|
+
this.types.set(businessTypeId, {
|
|
619
|
+
...existing,
|
|
620
|
+
deletedAt: this.nowIso()
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
async listVersions(businessTypeId) {
|
|
624
|
+
return [
|
|
625
|
+
...this.versions.values()
|
|
626
|
+
].filter((v) => v.businessTypeId === businessTypeId).sort((a, b) => a.version - b.version);
|
|
627
|
+
}
|
|
628
|
+
async findVersionById(versionId) {
|
|
629
|
+
return this.versions.get(versionId) ?? null;
|
|
630
|
+
}
|
|
631
|
+
async findCurrentDraft(businessTypeId) {
|
|
632
|
+
for (const v of this.versions.values()) {
|
|
633
|
+
if (v.businessTypeId === businessTypeId && v.publishedAt === null) return v;
|
|
634
|
+
}
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
async findLatestLive(businessTypeId) {
|
|
638
|
+
for (const v of this.versions.values()) {
|
|
639
|
+
if (v.businessTypeId === businessTypeId && v.publishedAt !== null && v.supersededAt === null) {
|
|
640
|
+
return v;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return null;
|
|
644
|
+
}
|
|
645
|
+
async createDraft(data) {
|
|
646
|
+
const draft = await this.findCurrentDraft(data.businessTypeId);
|
|
647
|
+
if (draft) {
|
|
648
|
+
throw new Error(`BusinessType '${data.businessTypeId}' hat bereits eine Draft-Version v${draft.version}`);
|
|
649
|
+
}
|
|
650
|
+
const all = await this.listVersions(data.businessTypeId);
|
|
651
|
+
const nextVersion = all.length === 0 ? 1 : Math.max(...all.map((v) => v.version)) + 1;
|
|
652
|
+
const now = this.nowIso();
|
|
653
|
+
const businessType = this.types.get(data.businessTypeId);
|
|
654
|
+
const bundles = data.bundles.map((b) => ({
|
|
655
|
+
bundleVersionId: b.bundleVersionId,
|
|
656
|
+
bundleKey: "",
|
|
657
|
+
bundleLabel: "",
|
|
658
|
+
bundleVersion: 0,
|
|
659
|
+
sortOrder: b.sortOrder ?? 0
|
|
660
|
+
}));
|
|
661
|
+
const row = {
|
|
662
|
+
id: this.genId("wwww"),
|
|
663
|
+
version: nextVersion,
|
|
664
|
+
baseVersionId: data.baseVersionId ?? null,
|
|
665
|
+
publishedAt: null,
|
|
666
|
+
supersededAt: null,
|
|
667
|
+
publishedChanges: null,
|
|
668
|
+
changeNote: data.changeNote ?? "",
|
|
669
|
+
nonRegressive: true,
|
|
670
|
+
createdByUserId: data.createdByUserId ?? null,
|
|
671
|
+
publishedByUserId: null,
|
|
672
|
+
validFrom: null,
|
|
673
|
+
validUntil: null,
|
|
674
|
+
createdAt: now,
|
|
675
|
+
updatedAt: now,
|
|
676
|
+
businessTypeId: data.businessTypeId,
|
|
677
|
+
businessTypeKey: businessType?.businessTypeKey ?? "",
|
|
678
|
+
label: businessType?.label ?? "",
|
|
679
|
+
quotaOverrides: data.quotaOverrides ?? {},
|
|
680
|
+
monthlyNet: data.monthlyNet ?? null,
|
|
681
|
+
yearlyNet: data.yearlyNet ?? null,
|
|
682
|
+
marketed: data.marketed ?? true,
|
|
683
|
+
bundles
|
|
684
|
+
};
|
|
685
|
+
this.versions.set(row.id, row);
|
|
686
|
+
return row;
|
|
687
|
+
}
|
|
688
|
+
async updateDraft(versionId, data) {
|
|
689
|
+
const existing = this.versions.get(versionId);
|
|
690
|
+
if (!existing) throw new Error(`BusinessTypeVersion '${versionId}' nicht gefunden`);
|
|
691
|
+
if (existing.publishedAt !== null) {
|
|
692
|
+
throw new Error(`BusinessTypeVersion '${versionId}' ist bereits published`);
|
|
693
|
+
}
|
|
694
|
+
const bundles = data.bundles ? data.bundles.map((b) => ({
|
|
695
|
+
bundleVersionId: b.bundleVersionId,
|
|
696
|
+
bundleKey: "",
|
|
697
|
+
bundleLabel: "",
|
|
698
|
+
bundleVersion: 0,
|
|
699
|
+
sortOrder: b.sortOrder ?? 0
|
|
700
|
+
})) : existing.bundles;
|
|
701
|
+
const updated = {
|
|
702
|
+
...existing,
|
|
703
|
+
quotaOverrides: data.quotaOverrides ?? existing.quotaOverrides,
|
|
704
|
+
monthlyNet: data.monthlyNet !== void 0 ? data.monthlyNet : existing.monthlyNet,
|
|
705
|
+
yearlyNet: data.yearlyNet !== void 0 ? data.yearlyNet : existing.yearlyNet,
|
|
706
|
+
marketed: data.marketed ?? existing.marketed,
|
|
707
|
+
changeNote: data.changeNote ?? existing.changeNote,
|
|
708
|
+
bundles,
|
|
709
|
+
updatedAt: this.nowIso()
|
|
710
|
+
};
|
|
711
|
+
this.versions.set(versionId, updated);
|
|
712
|
+
return updated;
|
|
713
|
+
}
|
|
714
|
+
async publishDraft(versionId, publishMeta) {
|
|
715
|
+
const draft = this.versions.get(versionId);
|
|
716
|
+
if (!draft) throw new Error(`BusinessTypeVersion '${versionId}' nicht gefunden`);
|
|
717
|
+
if (draft.publishedAt !== null) {
|
|
718
|
+
throw new Error(`BusinessTypeVersion '${versionId}' ist bereits published`);
|
|
719
|
+
}
|
|
720
|
+
const now = this.nowIso();
|
|
721
|
+
const previous = await this.findLatestLive(draft.businessTypeId);
|
|
722
|
+
if (previous) {
|
|
723
|
+
this.versions.set(previous.id, {
|
|
724
|
+
...previous,
|
|
725
|
+
supersededAt: now,
|
|
726
|
+
updatedAt: now
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
const published = {
|
|
730
|
+
...draft,
|
|
731
|
+
publishedAt: now,
|
|
732
|
+
publishedChanges: publishMeta.publishedChanges,
|
|
733
|
+
nonRegressive: publishMeta.nonRegressive,
|
|
734
|
+
publishedByUserId: publishMeta.publishedByUserId,
|
|
735
|
+
updatedAt: now
|
|
736
|
+
};
|
|
737
|
+
this.versions.set(versionId, published);
|
|
738
|
+
return published;
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
var FakeMarketingProjectionRepository = class {
|
|
742
|
+
static {
|
|
743
|
+
__name(this, "FakeMarketingProjectionRepository");
|
|
744
|
+
}
|
|
745
|
+
rows = /* @__PURE__ */ new Map();
|
|
746
|
+
nextId = 1;
|
|
747
|
+
genId() {
|
|
748
|
+
const n = this.nextId++;
|
|
749
|
+
const suffix = n.toString(16).padStart(12, "0");
|
|
750
|
+
return `mmmmaaaa-aaaa-aaaa-aaaa-${suffix}`;
|
|
751
|
+
}
|
|
752
|
+
nowIso() {
|
|
753
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
754
|
+
}
|
|
755
|
+
seed(row) {
|
|
756
|
+
this.rows.set(row.id, row);
|
|
757
|
+
}
|
|
758
|
+
clear() {
|
|
759
|
+
this.rows.clear();
|
|
760
|
+
}
|
|
761
|
+
async list(filter) {
|
|
762
|
+
return [
|
|
763
|
+
...this.rows.values()
|
|
764
|
+
].filter((r) => {
|
|
765
|
+
if (r.projectKey !== filter.projectKey) return false;
|
|
766
|
+
if (filter.targetType && r.targetType !== filter.targetType) return false;
|
|
767
|
+
if (filter.targetVersionId && r.targetVersionId !== filter.targetVersionId) return false;
|
|
768
|
+
if (filter.locale && r.locale !== filter.locale) return false;
|
|
769
|
+
return true;
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
async findById(id) {
|
|
773
|
+
return this.rows.get(id) ?? null;
|
|
774
|
+
}
|
|
775
|
+
async findByTarget(targetType, targetVersionId, locale) {
|
|
776
|
+
for (const r of this.rows.values()) {
|
|
777
|
+
if (r.targetType === targetType && r.targetVersionId === targetVersionId && r.locale === locale) {
|
|
778
|
+
return r;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
return null;
|
|
782
|
+
}
|
|
783
|
+
async create(data) {
|
|
784
|
+
const locale = data.locale ?? "de";
|
|
785
|
+
const existing = await this.findByTarget(data.targetType, data.targetVersionId, locale);
|
|
786
|
+
if (existing) {
|
|
787
|
+
throw new Error(`MarketingProjection f\xFCr ${data.targetType}/${data.targetVersionId}/${locale} existiert bereits`);
|
|
788
|
+
}
|
|
789
|
+
const now = this.nowIso();
|
|
790
|
+
const row = {
|
|
791
|
+
id: this.genId(),
|
|
792
|
+
projectKey: data.projectKey,
|
|
793
|
+
targetType: data.targetType,
|
|
794
|
+
targetVersionId: data.targetVersionId,
|
|
795
|
+
locale,
|
|
796
|
+
displayLabel: data.displayLabel,
|
|
797
|
+
description: data.description,
|
|
798
|
+
visible: data.visible ?? true,
|
|
799
|
+
badge: data.badge ?? "",
|
|
800
|
+
topFeatures: data.topFeatures ?? [],
|
|
801
|
+
trialEnabled: data.trialEnabled ?? false,
|
|
802
|
+
trialDays: data.trialDays ?? 30,
|
|
803
|
+
priceTag: data.priceTag ?? null,
|
|
804
|
+
ctaLabel: data.ctaLabel ?? null,
|
|
805
|
+
priority: data.priority ?? 0,
|
|
806
|
+
highlight: data.highlight ?? false,
|
|
807
|
+
createdAt: now,
|
|
808
|
+
updatedAt: now
|
|
809
|
+
};
|
|
810
|
+
this.rows.set(row.id, row);
|
|
811
|
+
return row;
|
|
812
|
+
}
|
|
813
|
+
async update(id, data) {
|
|
814
|
+
const existing = this.rows.get(id);
|
|
815
|
+
if (!existing) throw new Error(`MarketingProjection '${id}' nicht gefunden`);
|
|
816
|
+
const updated = {
|
|
817
|
+
...existing,
|
|
818
|
+
displayLabel: data.displayLabel ?? existing.displayLabel,
|
|
819
|
+
description: data.description ?? existing.description,
|
|
820
|
+
visible: data.visible ?? existing.visible,
|
|
821
|
+
badge: data.badge ?? existing.badge,
|
|
822
|
+
topFeatures: data.topFeatures ?? existing.topFeatures,
|
|
823
|
+
trialEnabled: data.trialEnabled ?? existing.trialEnabled,
|
|
824
|
+
trialDays: data.trialDays ?? existing.trialDays,
|
|
825
|
+
priceTag: data.priceTag !== void 0 ? data.priceTag : existing.priceTag,
|
|
826
|
+
ctaLabel: data.ctaLabel !== void 0 ? data.ctaLabel : existing.ctaLabel,
|
|
827
|
+
priority: data.priority ?? existing.priority,
|
|
828
|
+
highlight: data.highlight ?? existing.highlight,
|
|
829
|
+
updatedAt: this.nowIso()
|
|
830
|
+
};
|
|
831
|
+
this.rows.set(id, updated);
|
|
832
|
+
return updated;
|
|
833
|
+
}
|
|
834
|
+
async delete(id) {
|
|
835
|
+
if (!this.rows.has(id)) throw new Error(`MarketingProjection '${id}' nicht gefunden`);
|
|
836
|
+
this.rows.delete(id);
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
var FakePlanRepository = class {
|
|
840
|
+
static {
|
|
841
|
+
__name(this, "FakePlanRepository");
|
|
842
|
+
}
|
|
843
|
+
plans = /* @__PURE__ */ new Map();
|
|
844
|
+
versions = /* @__PURE__ */ new Map();
|
|
845
|
+
nextId = 1;
|
|
846
|
+
nextVersionId = 1;
|
|
847
|
+
genId() {
|
|
848
|
+
const n = this.nextId++;
|
|
849
|
+
const suffix = n.toString(16).padStart(12, "0");
|
|
850
|
+
return `dddd-aaaa-aaaa-aaaa-aaaa-${suffix}`;
|
|
851
|
+
}
|
|
852
|
+
genVersionId() {
|
|
853
|
+
const n = this.nextVersionId++;
|
|
854
|
+
const suffix = n.toString(16).padStart(12, "0");
|
|
855
|
+
return `eeee-aaaa-aaaa-aaaa-aaaa-${suffix}`;
|
|
856
|
+
}
|
|
857
|
+
nowIso() {
|
|
858
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
859
|
+
}
|
|
860
|
+
seed(row) {
|
|
861
|
+
this.plans.set(row.id, row);
|
|
862
|
+
}
|
|
863
|
+
seedVersion(row) {
|
|
864
|
+
this.versions.set(row.id, row);
|
|
865
|
+
}
|
|
866
|
+
clear() {
|
|
867
|
+
this.plans.clear();
|
|
868
|
+
this.versions.clear();
|
|
869
|
+
}
|
|
870
|
+
async list(filter) {
|
|
871
|
+
const excludeDeleted = filter.excludeDeleted ?? true;
|
|
872
|
+
const liveKeys = filter.onlyPublished ? new Set([
|
|
873
|
+
...this.versions.values()
|
|
874
|
+
].filter((v) => v.publishedAt != null && v.supersededAt == null).map((v) => v.planId)) : null;
|
|
875
|
+
return [
|
|
876
|
+
...this.plans.values()
|
|
877
|
+
].filter((p) => p.projectKey === filter.projectKey && (!excludeDeleted || p.deletedAt === null) && (!liveKeys || liveKeys.has(p.planKey)));
|
|
878
|
+
}
|
|
879
|
+
async findById(planId) {
|
|
880
|
+
return this.plans.get(planId) ?? null;
|
|
881
|
+
}
|
|
882
|
+
async findByKey(projectKey, planKey) {
|
|
883
|
+
for (const p of this.plans.values()) {
|
|
884
|
+
if (p.projectKey === projectKey && p.planKey === planKey && p.deletedAt === null) {
|
|
885
|
+
return p;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
return null;
|
|
889
|
+
}
|
|
890
|
+
async create(data) {
|
|
891
|
+
const now = this.nowIso();
|
|
892
|
+
const row = {
|
|
893
|
+
id: this.genId(),
|
|
894
|
+
projectKey: data.projectKey,
|
|
895
|
+
planKey: data.planKey,
|
|
896
|
+
label: data.label,
|
|
897
|
+
description: data.description ?? null,
|
|
898
|
+
icon: data.icon ?? null,
|
|
899
|
+
sortOrder: data.sortOrder ?? 0,
|
|
900
|
+
createdAt: now,
|
|
901
|
+
updatedAt: now,
|
|
902
|
+
deletedAt: null
|
|
903
|
+
};
|
|
904
|
+
this.plans.set(row.id, row);
|
|
905
|
+
return row;
|
|
906
|
+
}
|
|
907
|
+
async update(planId, data) {
|
|
908
|
+
const existing = this.plans.get(planId);
|
|
909
|
+
if (!existing) throw new Error(`Plan '${planId}' nicht gefunden`);
|
|
910
|
+
const updated = {
|
|
911
|
+
...existing,
|
|
912
|
+
label: data.label ?? existing.label,
|
|
913
|
+
description: data.description ?? existing.description,
|
|
914
|
+
icon: data.icon ?? existing.icon,
|
|
915
|
+
sortOrder: data.sortOrder ?? existing.sortOrder,
|
|
916
|
+
updatedAt: this.nowIso()
|
|
917
|
+
};
|
|
918
|
+
this.plans.set(planId, updated);
|
|
919
|
+
return updated;
|
|
920
|
+
}
|
|
921
|
+
async softDelete(planId) {
|
|
922
|
+
const existing = this.plans.get(planId);
|
|
923
|
+
if (!existing) throw new Error(`Plan '${planId}' nicht gefunden`);
|
|
924
|
+
const updated = {
|
|
925
|
+
...existing,
|
|
926
|
+
deletedAt: this.nowIso(),
|
|
927
|
+
updatedAt: this.nowIso()
|
|
928
|
+
};
|
|
929
|
+
this.plans.set(planId, updated);
|
|
930
|
+
}
|
|
931
|
+
async hardDelete(planId) {
|
|
932
|
+
const existing = this.plans.get(planId);
|
|
933
|
+
if (!existing) return;
|
|
934
|
+
this.plans.delete(planId);
|
|
935
|
+
for (const [id, v] of this.versions.entries()) {
|
|
936
|
+
if (v.planId === existing.planKey) this.versions.delete(id);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
// ─── Lifecycle (SPEC_V2 §11.1 M6 Pack 2a) ───
|
|
940
|
+
// Binding Plan ↔ PlanVersion: PlanVersion.planId === Plan.planKey
|
|
941
|
+
// (soft string reference). Lifecycle methods take planKey
|
|
942
|
+
// as input — the service resolves the plan UUID beforehand.
|
|
943
|
+
async listVersions(planKey) {
|
|
944
|
+
return [
|
|
945
|
+
...this.versions.values()
|
|
946
|
+
].filter((v) => v.planId === planKey).sort((a, b) => a.version - b.version);
|
|
947
|
+
}
|
|
948
|
+
async findVersionById(versionId) {
|
|
949
|
+
return this.versions.get(versionId) ?? null;
|
|
950
|
+
}
|
|
951
|
+
async findCurrentDraft(planKey) {
|
|
952
|
+
for (const v of this.versions.values()) {
|
|
953
|
+
if (v.planId === planKey && v.publishedAt === null) return v;
|
|
954
|
+
}
|
|
955
|
+
return null;
|
|
956
|
+
}
|
|
957
|
+
async findLatestLivePlanVersion(planKey) {
|
|
958
|
+
const now = Date.now();
|
|
959
|
+
const live = [
|
|
960
|
+
...this.versions.values()
|
|
961
|
+
].filter((v) => {
|
|
962
|
+
if (v.planId !== planKey) return false;
|
|
963
|
+
if (v.publishedAt === null) return false;
|
|
964
|
+
if (v.supersededAt !== null) return false;
|
|
965
|
+
if (v.endsAt) {
|
|
966
|
+
const ends = new Date(v.endsAt).getTime();
|
|
967
|
+
if (!Number.isNaN(ends) && ends <= now) return false;
|
|
968
|
+
}
|
|
969
|
+
return true;
|
|
970
|
+
});
|
|
971
|
+
live.sort((a, b) => b.version - a.version);
|
|
972
|
+
return live[0] ?? null;
|
|
973
|
+
}
|
|
974
|
+
async findActivePlanVersion(planKey, asOf = /* @__PURE__ */ new Date()) {
|
|
975
|
+
const t = asOf.getTime();
|
|
976
|
+
const dayStart = startOfUtcDay(asOf).getTime();
|
|
977
|
+
const matches = [
|
|
978
|
+
...this.versions.values()
|
|
979
|
+
].filter((v) => {
|
|
980
|
+
if (v.planId !== planKey) return false;
|
|
981
|
+
if (v.publishedAt === null) return false;
|
|
982
|
+
if (v.validFrom) {
|
|
983
|
+
const from = new Date(v.validFrom).getTime();
|
|
984
|
+
if (Number.isNaN(from) || from > t) return false;
|
|
985
|
+
}
|
|
986
|
+
if (v.validUntil) {
|
|
987
|
+
const until = new Date(v.validUntil).getTime();
|
|
988
|
+
if (!Number.isNaN(until) && until < dayStart) return false;
|
|
989
|
+
}
|
|
990
|
+
if (v.endsAt) {
|
|
991
|
+
const ends = new Date(v.endsAt).getTime();
|
|
992
|
+
if (!Number.isNaN(ends) && ends <= t) return false;
|
|
993
|
+
}
|
|
994
|
+
return true;
|
|
995
|
+
});
|
|
996
|
+
matches.sort((a, b) => {
|
|
997
|
+
const fa = a.validFrom ? new Date(a.validFrom).getTime() : 0;
|
|
998
|
+
const fb = b.validFrom ? new Date(b.validFrom).getTime() : 0;
|
|
999
|
+
if (fb !== fa) return fb - fa;
|
|
1000
|
+
return b.version - a.version;
|
|
1001
|
+
});
|
|
1002
|
+
return matches[0] ?? null;
|
|
1003
|
+
}
|
|
1004
|
+
async createPlanVersionDraft(data) {
|
|
1005
|
+
const planKey = data.planId;
|
|
1006
|
+
const versions = [
|
|
1007
|
+
...this.versions.values()
|
|
1008
|
+
].filter((v) => v.planId === planKey);
|
|
1009
|
+
const nextVersion = versions.reduce((max, v) => Math.max(max, v.version), 0) + 1;
|
|
1010
|
+
const now = this.nowIso();
|
|
1011
|
+
const row = {
|
|
1012
|
+
id: this.genVersionId(),
|
|
1013
|
+
planId: planKey,
|
|
1014
|
+
version: nextVersion,
|
|
1015
|
+
baseVersionId: data.baseVersionId ?? null,
|
|
1016
|
+
features: data.features,
|
|
1017
|
+
bundles: data.bundles ?? [],
|
|
1018
|
+
quotas: data.quotas,
|
|
1019
|
+
monthlyNet: data.monthlyNet,
|
|
1020
|
+
yearlyNet: data.yearlyNet,
|
|
1021
|
+
marketed: data.marketed ?? true,
|
|
1022
|
+
publishedAt: null,
|
|
1023
|
+
supersededAt: null,
|
|
1024
|
+
publishedChanges: null,
|
|
1025
|
+
changeNote: data.changeNote ?? "",
|
|
1026
|
+
nonRegressive: true,
|
|
1027
|
+
validFrom: data.validFrom ?? null,
|
|
1028
|
+
validUntil: data.validUntil ?? null,
|
|
1029
|
+
endsAt: null,
|
|
1030
|
+
createdByUserId: data.createdByUserId ?? null,
|
|
1031
|
+
publishedByUserId: null,
|
|
1032
|
+
createdAt: now,
|
|
1033
|
+
updatedAt: now
|
|
1034
|
+
};
|
|
1035
|
+
this.versions.set(row.id, row);
|
|
1036
|
+
return row;
|
|
1037
|
+
}
|
|
1038
|
+
async updatePlanVersionDraft(versionId, data) {
|
|
1039
|
+
const existing = this.versions.get(versionId);
|
|
1040
|
+
if (!existing) throw new Error(`PlanVersion '${versionId}' nicht gefunden`);
|
|
1041
|
+
const updated = {
|
|
1042
|
+
...existing,
|
|
1043
|
+
features: data.features ?? existing.features,
|
|
1044
|
+
bundles: data.bundles ?? existing.bundles ?? [],
|
|
1045
|
+
quotas: data.quotas ?? existing.quotas,
|
|
1046
|
+
monthlyNet: data.monthlyNet ?? existing.monthlyNet,
|
|
1047
|
+
yearlyNet: data.yearlyNet ?? existing.yearlyNet,
|
|
1048
|
+
marketed: data.marketed ?? existing.marketed,
|
|
1049
|
+
changeNote: data.changeNote ?? existing.changeNote,
|
|
1050
|
+
updatedAt: this.nowIso()
|
|
1051
|
+
};
|
|
1052
|
+
this.versions.set(versionId, updated);
|
|
1053
|
+
return updated;
|
|
1054
|
+
}
|
|
1055
|
+
async publishPlanVersionDraft(versionId, publishMeta) {
|
|
1056
|
+
const draft = this.versions.get(versionId);
|
|
1057
|
+
if (!draft) throw new Error(`PlanVersion '${versionId}' nicht gefunden`);
|
|
1058
|
+
const planKey = draft.planId;
|
|
1059
|
+
const now = this.nowIso();
|
|
1060
|
+
const dayMs = 24 * 60 * 60 * 1e3;
|
|
1061
|
+
const inheritedValidUntil = new Date(publishMeta.validFrom.getTime() - dayMs).toISOString();
|
|
1062
|
+
for (const v of this.versions.values()) {
|
|
1063
|
+
if (v.planId === planKey && v.publishedAt !== null && v.supersededAt === null && v.id !== versionId) {
|
|
1064
|
+
this.versions.set(v.id, {
|
|
1065
|
+
...v,
|
|
1066
|
+
supersededAt: now,
|
|
1067
|
+
validUntil: v.validUntil ?? inheritedValidUntil,
|
|
1068
|
+
updatedAt: now
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
const published = {
|
|
1073
|
+
...draft,
|
|
1074
|
+
publishedAt: now,
|
|
1075
|
+
publishedChanges: publishMeta.publishedChanges,
|
|
1076
|
+
nonRegressive: publishMeta.nonRegressive,
|
|
1077
|
+
publishedByUserId: publishMeta.publishedByUserId,
|
|
1078
|
+
validFrom: publishMeta.validFrom.toISOString(),
|
|
1079
|
+
validUntil: publishMeta.validUntil ? publishMeta.validUntil.toISOString() : null,
|
|
1080
|
+
updatedAt: now
|
|
1081
|
+
};
|
|
1082
|
+
this.versions.set(versionId, published);
|
|
1083
|
+
return published;
|
|
1084
|
+
}
|
|
1085
|
+
async deletePlanVersionDraft(versionId) {
|
|
1086
|
+
const existing = this.versions.get(versionId);
|
|
1087
|
+
if (!existing) return;
|
|
1088
|
+
if (existing.publishedAt !== null) {
|
|
1089
|
+
throw new Error(`FakePlanRepository: PlanVersion '${versionId}' ist bereits published \u2014 Discard nicht erlaubt`);
|
|
1090
|
+
}
|
|
1091
|
+
this.versions.delete(versionId);
|
|
1092
|
+
}
|
|
1093
|
+
async terminate(versionId, endsAt) {
|
|
1094
|
+
const existing = this.versions.get(versionId);
|
|
1095
|
+
if (!existing) {
|
|
1096
|
+
throw new Error(`FakePlanRepository: PlanVersion '${versionId}' nicht gefunden`);
|
|
1097
|
+
}
|
|
1098
|
+
const updated = {
|
|
1099
|
+
...existing,
|
|
1100
|
+
endsAt: endsAt.toISOString(),
|
|
1101
|
+
updatedAt: this.nowIso()
|
|
1102
|
+
};
|
|
1103
|
+
this.versions.set(versionId, updated);
|
|
1104
|
+
return updated;
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
|
|
1108
|
+
// src/testing/create-saas-platform-test-module.ts
|
|
1109
|
+
import { Module } from "@nestjs/common";
|
|
1110
|
+
function _ts_decorate(decorators, target, key, desc) {
|
|
1111
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
1112
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
1113
|
+
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;
|
|
1114
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
1115
|
+
}
|
|
1116
|
+
__name(_ts_decorate, "_ts_decorate");
|
|
1117
|
+
var StubMfaPort = class {
|
|
1118
|
+
static {
|
|
1119
|
+
__name(this, "StubMfaPort");
|
|
1120
|
+
}
|
|
1121
|
+
secrets = /* @__PURE__ */ new Map();
|
|
1122
|
+
async getSecret(userId) {
|
|
1123
|
+
return this.secrets.get(userId) ?? null;
|
|
1124
|
+
}
|
|
1125
|
+
async setSecret(userId, secret) {
|
|
1126
|
+
if (secret === null) this.secrets.delete(userId);
|
|
1127
|
+
else this.secrets.set(userId, secret);
|
|
1128
|
+
}
|
|
1129
|
+
async isEnabled(userId) {
|
|
1130
|
+
return this.secrets.has(userId);
|
|
1131
|
+
}
|
|
1132
|
+
};
|
|
1133
|
+
var StubAuditPort = class {
|
|
1134
|
+
static {
|
|
1135
|
+
__name(this, "StubAuditPort");
|
|
1136
|
+
}
|
|
1137
|
+
calls = [];
|
|
1138
|
+
async write(input) {
|
|
1139
|
+
this.calls.push(input);
|
|
1140
|
+
}
|
|
1141
|
+
};
|
|
1142
|
+
var StubRlsBypassPort = class {
|
|
1143
|
+
static {
|
|
1144
|
+
__name(this, "StubRlsBypassPort");
|
|
1145
|
+
}
|
|
1146
|
+
async runWithBypass(fn) {
|
|
1147
|
+
return fn();
|
|
1148
|
+
}
|
|
1149
|
+
};
|
|
1150
|
+
function createSaasPlatformTestModule(options) {
|
|
1151
|
+
const defaultPlanId = options.defaultPlanId ?? options.planCatalog.plans?.[0]?.id;
|
|
1152
|
+
let SaasPlatformTestHost = class SaasPlatformTestHost {
|
|
1153
|
+
static {
|
|
1154
|
+
__name(this, "SaasPlatformTestHost");
|
|
1155
|
+
}
|
|
1156
|
+
};
|
|
1157
|
+
SaasPlatformTestHost = _ts_decorate([
|
|
1158
|
+
Module({
|
|
1159
|
+
imports: [
|
|
1160
|
+
SaasPlatformModule.forRoot({
|
|
1161
|
+
planCatalog: options.planCatalog,
|
|
1162
|
+
controller: {
|
|
1163
|
+
guards: []
|
|
1164
|
+
},
|
|
1165
|
+
adapters: {
|
|
1166
|
+
mfa: options.overrides?.mfa ?? new StubMfaPort(),
|
|
1167
|
+
audit: options.overrides?.audit ?? new StubAuditPort(),
|
|
1168
|
+
rlsBypass: options.overrides?.rlsBypass ?? new StubRlsBypassPort()
|
|
1169
|
+
},
|
|
1170
|
+
defaultPlanId,
|
|
1171
|
+
quotaProviders: options.quotaProviders
|
|
1172
|
+
})
|
|
1173
|
+
]
|
|
1174
|
+
})
|
|
1175
|
+
], SaasPlatformTestHost);
|
|
1176
|
+
return {
|
|
1177
|
+
module: SaasPlatformTestHost
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
__name(createSaasPlatformTestModule, "createSaasPlatformTestModule");
|
|
1181
|
+
export {
|
|
1182
|
+
FakeBundleRepository,
|
|
1183
|
+
FakeBusinessTypeRepository,
|
|
1184
|
+
FakeMarketingProjectionRepository,
|
|
1185
|
+
FakePlanRepository,
|
|
1186
|
+
FakePlanVersionRepository,
|
|
1187
|
+
FakeSubscriptionBundleRepository,
|
|
1188
|
+
FakeSubscriptionContractRepository,
|
|
1189
|
+
FakeSubscriptionRepository,
|
|
1190
|
+
FakeTransactionRunner,
|
|
1191
|
+
StubAuditPort,
|
|
1192
|
+
StubMfaPort,
|
|
1193
|
+
StubRlsBypassPort,
|
|
1194
|
+
createSaasPlatformTestModule
|
|
1195
|
+
};
|