@saasicat/nest 0.8.0 → 0.10.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.
Files changed (95) hide show
  1. package/dist/_entries.cjs +17678 -0
  2. package/dist/admin/index.cjs +4 -1274
  3. package/dist/admin/index.d.cts +10 -171
  4. package/dist/admin/index.d.ts +10 -171
  5. package/dist/admin/index.js +13 -4
  6. package/dist/admin-resources.module-BZN_yBFj.d.cts +40 -0
  7. package/dist/admin-resources.module-Cms8MY6h.d.ts +40 -0
  8. package/dist/admin-stats.module-15ac5KY3.d.ts +201 -0
  9. package/dist/admin-stats.module-v4SVvWzj.d.cts +201 -0
  10. package/dist/billing/index.cjs +4 -5128
  11. package/dist/billing/index.d.cts +8 -719
  12. package/dist/billing/index.d.ts +8 -719
  13. package/dist/billing/index.js +7 -10
  14. package/dist/catalog/index.cjs +4 -5624
  15. package/dist/catalog/index.d.cts +7 -427
  16. package/dist/catalog/index.d.ts +7 -427
  17. package/dist/catalog/index.js +14 -12
  18. package/dist/catalog.module-BKObceo0.d.ts +103 -0
  19. package/dist/catalog.module-Dw2Hrbyu.d.cts +103 -0
  20. package/dist/checkout-offer/index.cjs +4 -807
  21. package/dist/checkout-offer/index.d.cts +5 -84
  22. package/dist/checkout-offer/index.d.ts +5 -84
  23. package/dist/checkout-offer/index.js +3 -2
  24. package/dist/checkout-offer.module-Y0hDOVrq.d.ts +85 -0
  25. package/dist/checkout-offer.module-ZYF5IsDY.d.cts +85 -0
  26. package/dist/{chunk-5DDCSGZC.js → chunk-33TP26MR.js} +144 -356
  27. package/dist/{chunk-CK54YKVK.js → chunk-75JABGZK.js} +16 -16
  28. package/dist/{chunk-HRU3CP57.js → chunk-BOADBIGB.js} +1 -1
  29. package/dist/{chunk-ZLUGQE5D.js → chunk-E7AJ42AB.js} +8 -8
  30. package/dist/{chunk-6LP7CDS2.js → chunk-F3VV6EXG.js} +18 -13
  31. package/dist/{chunk-PLDHI6NA.js → chunk-FVT7KTUI.js} +258 -3
  32. package/dist/{chunk-RZU4HIAN.js → chunk-GBDUH6DO.js} +26 -23
  33. package/dist/{chunk-K3OPNVFR.js → chunk-HBFU5AN3.js} +5 -9
  34. package/dist/chunk-HWGIMNWR.js +0 -0
  35. package/dist/chunk-IRWZC3GC.js +755 -0
  36. package/dist/chunk-L4KTQ6CL.js +20 -0
  37. package/dist/{chunk-T5SXXAKG.js → chunk-LZHLGUQI.js} +1 -1
  38. package/dist/{chunk-YHHE4KDA.js → chunk-PUXLPUVH.js} +351 -21
  39. package/dist/chunk-TXDH4FUA.js +211 -0
  40. package/dist/chunk-UF53DWWR.js +0 -0
  41. package/dist/{chunk-AJ3EPELS.js → chunk-V34AA7C7.js} +1 -1
  42. package/dist/chunk-WJ6BMDL3.js +313 -0
  43. package/dist/{chunk-M6CZQYPY.js → chunk-Z3FWN3HJ.js} +2 -2
  44. package/dist/discovery/index.cjs +4 -588
  45. package/dist/discovery/index.js +3 -3
  46. package/dist/{enforce-quota.interceptor-cv7r80VM.d.cts → enforce-quota.interceptor-BhkX5nq2.d.cts} +127 -12
  47. package/dist/{enforce-quota.interceptor-Dj1Kusr5.d.ts → enforce-quota.interceptor-CcqZc0Yu.d.ts} +127 -12
  48. package/dist/entitlement/index.cjs +4 -780
  49. package/dist/entitlement/index.d.cts +3 -1
  50. package/dist/entitlement/index.d.ts +3 -1
  51. package/dist/entitlement/index.js +2 -3
  52. package/dist/index.cjs +4 -14975
  53. package/dist/index.d.cts +22 -54
  54. package/dist/index.d.ts +22 -54
  55. package/dist/index.js +88 -369
  56. package/dist/module-CSN8j8Tp.d.cts +39 -0
  57. package/dist/module-DgYJur6f.d.ts +39 -0
  58. package/dist/plan-versions.service-BB36Q1bg.d.ts +329 -0
  59. package/dist/plan-versions.service-CUR2Whyr.d.cts +329 -0
  60. package/dist/platform/index.cjs +4 -2779
  61. package/dist/platform/index.d.cts +50 -8
  62. package/dist/platform/index.d.ts +50 -8
  63. package/dist/platform/index.js +116 -12
  64. package/dist/promo/index.cjs +4 -995
  65. package/dist/promo/index.d.cts +48 -46
  66. package/dist/promo/index.d.ts +48 -46
  67. package/dist/promo/index.js +10 -2
  68. package/dist/rate-limit.guard-dntqAlfZ.d.cts +19 -0
  69. package/dist/rate-limit.guard-dntqAlfZ.d.ts +19 -0
  70. package/dist/registration/index.cjs +4 -1655
  71. package/dist/registration/index.js +1 -1
  72. package/dist/setup.service-BPwJ7My_.d.cts +45 -0
  73. package/dist/setup.service-D2mrdDDB.d.ts +45 -0
  74. package/dist/subscription-bundle-preview.service-BkJXHAeP.d.cts +438 -0
  75. package/dist/subscription-bundle-preview.service-DBbrE_cw.d.ts +438 -0
  76. package/dist/subscription-bundles.module-BQmoozm5.d.ts +291 -0
  77. package/dist/subscription-bundles.module-DCpV-Pb5.d.cts +291 -0
  78. package/dist/subscription-contract/index.cjs +4 -471
  79. package/dist/subscription-contract/index.d.cts +5 -13
  80. package/dist/subscription-contract/index.d.ts +5 -13
  81. package/dist/subscription-contract.module-CG9fPKAr.d.cts +14 -0
  82. package/dist/subscription-contract.module-CVebZzHR.d.ts +14 -0
  83. package/dist/testing/index.cjs +4 -3756
  84. package/dist/testing/index.d.cts +9 -4
  85. package/dist/testing/index.d.ts +9 -4
  86. package/dist/testing/index.js +13 -7
  87. package/package.json +5 -5
  88. package/dist/admin-manifest.config-DyrQNT7M.d.cts +0 -10
  89. package/dist/admin-manifest.config-DyrQNT7M.d.ts +0 -10
  90. package/dist/admin-manifest.service-Cp6uwvmt.d.cts +0 -33
  91. package/dist/admin-manifest.service-UGgzR1x-.d.ts +0 -33
  92. package/dist/chunk-52UTQRD3.js +0 -326
  93. package/dist/chunk-N3WANKSA.js +0 -20
  94. package/dist/chunk-Q53N43LQ.js +0 -13
  95. /package/dist/{chunk-2FR6ZL7R.js → chunk-DBZGV3HC.js} +0 -0
@@ -0,0 +1,329 @@
1
+ import { BundleRepository, DiscoverySnapshot, SubscriptionRepository, PlanRepository, CatalogEntryRepository, BundleRow, BundleVersionRow, CreateBundleData, UpdateBundleData, CreateBundleVersionDraftData, BundleVersionMutationResult, UpdateBundleVersionDraftData, PublishBundleVersionData, MarketingProjectionRepository, MarketingProjectionFilter, MarketingProjectionRow, CreateMarketingProjectionData, UpdateMarketingProjectionData, FeatureUiRegistry, CapabilityCatalogEntryRow, FeatureCatalogEntryRow, QuotaCatalogEntryRow, ReviewCatalogEntryData, CatalogEntryI18n, UpdateCatalogEntryBaseData, SyncDiscoveryResult, PromotionRepository, PromotionRow, CreatePromotionData, UpdatePromotionData, MarketingSettingsRepository, MarketingSettingsRow, UpdateMarketingSettingsData, PublicMarketingCatalogResponse, PlanRow, CreatePlanData, UpdatePlanData, PlanVersionRow, CreatePlanVersionDraftData, PlanVersionMutationResult, UpdatePlanVersionDraftData, PublishPlanVersionData } from '@saasicat/types';
2
+ import { OnApplicationBootstrap } from '@nestjs/common';
3
+ import { c as DiscoveryScanner } from './discovery.scanner-DLgEvuoy.js';
4
+
5
+ /**
6
+ * Configuration for the mutating operations.
7
+ */
8
+ interface CatalogServiceConfig {
9
+ /** Active mode for the strict-mode check. Default `'blocking'` (#12). */
10
+ strictModeCheckMode?: 'warn-only' | 'blocking';
11
+ /**
12
+ * Discovery→Catalog auto-sync at boot (#12). Default `true` — runs when a
13
+ * `DiscoverySnapshot` is available. Set to `false` to disable the boot
14
+ * sync; the manual `POST /admin/catalog/discovery/sync` is unaffected.
15
+ */
16
+ autoSyncDiscoveryAtBoot?: boolean;
17
+ /**
18
+ * Feature keys the catalog is deliberately allowed to carry WITHOUT them
19
+ * existing in the discovery snapshot (= in code via @ImplementsCapability) —
20
+ * marketed non-code features such as support SLAs (e.g. PRIORITY_SUPPORT).
21
+ * The strict-mode check does NOT report them as BUNDLE_/PLAN_FEATURE_UNKNOWN.
22
+ * Use sparingly: this is the only legitimate exception to "code is the
23
+ * source of truth"; not-yet-built features do NOT belong here but should be
24
+ * removed from the catalog until they are implemented.
25
+ */
26
+ marketedOnlyFeatures?: string[];
27
+ }
28
+ declare class BundlesService {
29
+ private readonly repo;
30
+ private readonly snapshot;
31
+ private readonly subscriptions;
32
+ private readonly planRepo;
33
+ private readonly scanner;
34
+ private readonly catalogEntries;
35
+ private readonly logger;
36
+ private mode;
37
+ private readonly marketedOnly;
38
+ constructor(repo: BundleRepository, snapshot?: DiscoverySnapshot | null, config?: CatalogServiceConfig, subscriptions?: SubscriptionRepository | null, planRepo?: PlanRepository | null, scanner?: DiscoveryScanner | null, catalogEntries?: CatalogEntryRepository | null);
39
+ listBundles(projectKey: string): Promise<BundleRow[]>;
40
+ getBundle(bundleId: string): Promise<{
41
+ bundle: BundleRow;
42
+ versions: BundleVersionRow[];
43
+ }>;
44
+ createBundle(data: CreateBundleData): Promise<BundleRow>;
45
+ updateBundle(bundleId: string, data: UpdateBundleData): Promise<BundleRow>;
46
+ softDeleteBundle(bundleId: string): Promise<void>;
47
+ /**
48
+ * Hard-deletes a draft BundleVersion (`publishedAt === null`) from the
49
+ * DB. Published versions stay immutable (contract-protection P1) — the
50
+ * service throws 422 with code `BUNDLE_VERSION_ALREADY_PUBLISHED`.
51
+ */
52
+ discardBundleDraft(versionId: string): Promise<void>;
53
+ listBundleVersions(bundleId: string): Promise<BundleVersionRow[]>;
54
+ getBundleVersion(versionId: string): Promise<BundleVersionRow>;
55
+ /**
56
+ * Annotates each BundleVersion with `isLatestInChain` (highest version
57
+ * number in the chain) + `subscriptionCount` so that UI and backend make
58
+ * the same editability decision. `subscriptionCount` stays `undefined`
59
+ * when no SubscriptionRepository is registered or the method was not
60
+ * implemented — `isVersionEditable` interprets that fail-closed
61
+ * (= frozen).
62
+ */
63
+ private annotateEditability;
64
+ /**
65
+ * Creates a new draft BundleVersion. Throws when a draft already exists
66
+ * for the same bundle (repository constraint).
67
+ *
68
+ * The strict-mode check runs on the input; in `warn-only` warnings are
69
+ * returned, in `blocking` the service throws HTTP 422.
70
+ */
71
+ createBundleDraft(data: CreateBundleVersionDraftData): Promise<BundleVersionMutationResult>;
72
+ /**
73
+ * Updates a draft version or — as a deliberate relaxation of
74
+ * contract-protection P1 — a published-but-future BundleVersion that is
75
+ * latest-in-chain and does not yet bind any subscription. Once
76
+ * `validFrom` is reached or a booking references it, the version freezes
77
+ * again. The concrete decision is made by the `isVersionEditable` helper,
78
+ * which is mirrored by the UI.
79
+ */
80
+ updateBundleDraft(versionId: string, data: UpdateBundleVersionDraftData): Promise<BundleVersionMutationResult>;
81
+ /**
82
+ * Publishes a draft version atomically. Steps:
83
+ * 1. Strict check on the draft definition (warn-only or blocking)
84
+ * 2. Enforce validFrom requirement + ordering against the predecessor
85
+ * (SPEC_V2 §4.2 + §11.1 M6 Pack 2c, analogous to PlanVersion)
86
+ * 3. Compute the diff against the predecessor version (latest live) →
87
+ * `publishedChanges` + `nonRegressive` via `classifyBundleVersionDiff`
88
+ * 4. Call Repository.publishDraft with the diff result + validity dates
89
+ * (transaction; auto-succession sets the previous validUntil)
90
+ */
91
+ publishBundleVersion(versionId: string, publishMeta: PublishBundleVersionData): Promise<BundleVersionMutationResult>;
92
+ private runStrictCheck;
93
+ private gateOrPass;
94
+ private assertBundleVersionWindowUpdate;
95
+ private findPreviousBundleVersion;
96
+ private parseOptionalBundleDate;
97
+ private parseRequiredBundleDate;
98
+ }
99
+
100
+ declare class MarketingProjectionsService {
101
+ private readonly repo;
102
+ constructor(repo: MarketingProjectionRepository);
103
+ list(filter: MarketingProjectionFilter): Promise<MarketingProjectionRow[]>;
104
+ getById(id: string): Promise<MarketingProjectionRow>;
105
+ create(data: CreateMarketingProjectionData): Promise<MarketingProjectionRow>;
106
+ update(id: string, data: UpdateMarketingProjectionData): Promise<MarketingProjectionRow>;
107
+ delete(id: string): Promise<void>;
108
+ }
109
+
110
+ declare class CatalogEntriesService implements OnApplicationBootstrap {
111
+ private readonly repo;
112
+ private readonly snapshot;
113
+ private readonly scanner;
114
+ private readonly featureUiRegistry;
115
+ private readonly logger;
116
+ private readonly autoSyncAtBoot;
117
+ constructor(repo: CatalogEntryRepository, snapshot?: DiscoverySnapshot | null, config?: CatalogServiceConfig, scanner?: DiscoveryScanner | null, featureUiRegistry?: FeatureUiRegistry | null);
118
+ /**
119
+ * Mirrors the discovery snapshot into the catalog entries at boot
120
+ * (discovery-as-SSOT, #12). Runs only when a snapshot is available and
121
+ * the kill switch `autoSyncDiscoveryAtBoot` is not set to `false`. A
122
+ * sync error deliberately does not abort the boot — the manual endpoint
123
+ * `POST /admin/catalog/discovery/sync` remains as a fallback.
124
+ */
125
+ onApplicationBootstrap(): Promise<void>;
126
+ listCapabilities(projectKey: string, codeStatus?: string): Promise<CapabilityCatalogEntryRow[]>;
127
+ listFeatures(projectKey: string, discoveryStatus?: string): Promise<FeatureCatalogEntryRow[]>;
128
+ listQuotas(projectKey: string, discoveryStatus?: string): Promise<QuotaCatalogEntryRow[]>;
129
+ /**
130
+ * Approval transition of a feature (#20). Validates the state machine;
131
+ * `approved` persists the approval signature from the current
132
+ * snapshot, `pending` (revoke/reactivate) deletes it, `outdated`/
133
+ * `obsolete` keep the last approval as history.
134
+ */
135
+ reviewFeature(projectKey: string, featureKey: string, data: ReviewCatalogEntryData, reviewedBy: string | null): Promise<FeatureCatalogEntryRow>;
136
+ /** Approval transition of a quota — same state machine as features (#20). */
137
+ reviewQuota(projectKey: string, quotaKey: string, data: ReviewCatalogEntryData, reviewedBy: string | null): Promise<QuotaCatalogEntryRow>;
138
+ private resolveReviewUpdate;
139
+ setFeatureI18n(projectKey: string, featureKey: string, i18n: CatalogEntryI18n): Promise<FeatureCatalogEntryRow>;
140
+ setQuotaI18n(projectKey: string, quotaKey: string, i18n: CatalogEntryI18n): Promise<QuotaCatalogEntryRow>;
141
+ /** Sets the editable default-locale label/description of a feature. */
142
+ setFeatureBase(projectKey: string, featureKey: string, data: UpdateCatalogEntryBaseData): Promise<FeatureCatalogEntryRow>;
143
+ /** Sets the editable default-locale label/description of a quota. */
144
+ setQuotaBase(projectKey: string, quotaKey: string, data: UpdateCatalogEntryBaseData): Promise<QuotaCatalogEntryRow>;
145
+ /**
146
+ * Synchronizes the catalog entries from a discovery snapshot.
147
+ * Idempotent — can run against the same snapshot any number of times.
148
+ */
149
+ syncFromSnapshot(snapshot: DiscoverySnapshot): Promise<SyncDiscoveryResult>;
150
+ /**
151
+ * `replaced` semantics (#39): if a key disappears from the snapshot AND
152
+ * another snapshot key claims it via `replaces`, the old entry gets
153
+ * the successor pointer — `obsolete` (from `retireMissing`)
154
+ * + `successorKey` instead of a bare `obsolete`. If a key reappears in
155
+ * the code, an existing pointer is deleted. A missing key
156
+ * without a claimant remains untouched (deleted without replacement).
157
+ */
158
+ private applySuccessorPointers;
159
+ }
160
+
161
+ declare class PromotionsService {
162
+ private readonly repo;
163
+ constructor(repo: PromotionRepository);
164
+ list(projectKey: string): Promise<PromotionRow[]>;
165
+ getById(id: string): Promise<PromotionRow>;
166
+ create(data: CreatePromotionData): Promise<PromotionRow>;
167
+ update(id: string, data: UpdatePromotionData): Promise<PromotionRow>;
168
+ delete(id: string): Promise<void>;
169
+ }
170
+
171
+ declare class MarketingSettingsService {
172
+ private readonly repo;
173
+ constructor(repo: MarketingSettingsRepository);
174
+ /** Returns the stored config or `null` (in which case the full pool applies). */
175
+ get(projectKey: string): Promise<MarketingSettingsRow | null>;
176
+ upsert(projectKey: string, data: UpdateMarketingSettingsData): Promise<MarketingSettingsRow>;
177
+ }
178
+
179
+ declare class PublicMarketingCatalogService {
180
+ private readonly planRepo;
181
+ private readonly marketingRepo;
182
+ private readonly promotionRepo;
183
+ private readonly catalogEntryRepo;
184
+ private readonly bundleRepo;
185
+ constructor(planRepo: PlanRepository, marketingRepo: MarketingProjectionRepository, promotionRepo: PromotionRepository, catalogEntryRepo?: CatalogEntryRepository | null, bundleRepo?: BundleRepository | null);
186
+ getCatalog(projectKey: string, locale: string, currency: string, vatRate: number, asOf?: Date): Promise<PublicMarketingCatalogResponse>;
187
+ /**
188
+ * Loads all published-and-live bundle versions for a project and
189
+ * filters on `marketed === true`. Without a registered
190
+ * `BundleRepository` (apps without the SuperAdmin bundle editor) the
191
+ * list is empty — the public catalog stays plan-only.
192
+ */
193
+ private loadMarketedBundles;
194
+ /**
195
+ * Maps BundleVersionRow → PublicMarketingBundle. Locale resolution
196
+ * runs via `MarketingProjectionRepository.findByTarget('BUNDLE',
197
+ * bundleVersionId, locale)` — on a miss it falls back to
198
+ * `DEFAULT_LOCALE`. If the fallback is missing too, label/description
199
+ * come directly from the bundle root (denormalized in `BundleVersionRow`).
200
+ *
201
+ * If the marketing projection is `visible === false`, the bundle is
202
+ * hidden from the public response (preparation phase).
203
+ */
204
+ private toPublicBundle;
205
+ /**
206
+ * Labels for bundle features ∪ their requires (#48): `comparison.features`
207
+ * only covers the plan-feature union — bundle-only features would
208
+ * otherwise get no label in the public payload. Curated keys only;
209
+ * without a CatalogEntryRepository the map stays empty (frontends fall
210
+ * back to the key).
211
+ */
212
+ private resolveBundleFeatureLabels;
213
+ /** Loads the translated feature/quota labels (+ sortOrder) for a locale. */
214
+ private loadLabelMeta;
215
+ /**
216
+ * Resolves the displayed label of a top feature: the `label` override
217
+ * wins; otherwise the translated label of the referenced `key`.
218
+ */
219
+ private resolveTopFeature;
220
+ /** Row definitions of the comparison matrix (feature/quota union + labels). */
221
+ private buildComparison;
222
+ private resolvePromo;
223
+ }
224
+
225
+ declare class PlansService {
226
+ private readonly repo;
227
+ private readonly subscriptions;
228
+ constructor(repo: PlanRepository, subscriptions?: SubscriptionRepository | null);
229
+ /**
230
+ * Platform-wide count of active (ACTIVE/TRIAL) subscriptions per `planKey`,
231
+ * across versions and across all tenants (RLS-exempt). Plans without a
232
+ * subscription are absent from the map. Returns `{}` when no
233
+ * `SubscriptionRepository` is registered or it does not implement
234
+ * `countActiveByPlanKey`.
235
+ */
236
+ getTenantCounts(projectKey: string): Promise<Record<string, number>>;
237
+ listPlans(projectKey: string, opts?: {
238
+ onlyPublished?: boolean;
239
+ }): Promise<PlanRow[]>;
240
+ getPlan(planId: string): Promise<PlanRow>;
241
+ createPlan(data: CreatePlanData): Promise<PlanRow>;
242
+ updatePlan(planId: string, data: UpdatePlanData): Promise<PlanRow>;
243
+ softDeletePlan(planId: string): Promise<void>;
244
+ /**
245
+ * Hard-deletes the plan master record. Only allowed for plans without
246
+ * `PlanVersion` entries — published versions are immutable under
247
+ * contract protection P1 (existing subscriptions reference them). The
248
+ * caller must discard drafts beforehand via `DELETE plan-versions/:id`.
249
+ *
250
+ * If the repository does not implement `hardDelete`, the endpoint
251
+ * responds with 422 `PLAN_HARD_DELETE_NOT_IMPLEMENTED` — consumers can
252
+ * then fall back to `softDelete`.
253
+ */
254
+ hardDeletePlan(planId: string): Promise<void>;
255
+ /**
256
+ * Platform invariant (contract protection P1): once a plan has ever had a
257
+ * published version, its plan master record is retained — regardless of
258
+ * whether the version has since been superseded. Subscriptions still
259
+ * reference it via the version, and the audit log must be able to resolve
260
+ * the plan.
261
+ */
262
+ private assertNoPublishedVersions;
263
+ private assertNoDrafts;
264
+ }
265
+
266
+ declare class PlanVersionsService {
267
+ private readonly repo;
268
+ private readonly snapshot;
269
+ private readonly subscriptions;
270
+ private readonly scanner;
271
+ private readonly catalogEntries;
272
+ private readonly logger;
273
+ private mode;
274
+ private readonly marketedOnly;
275
+ constructor(repo: PlanRepository, snapshot?: DiscoverySnapshot | null, config?: CatalogServiceConfig, subscriptions?: SubscriptionRepository | null, scanner?: DiscoveryScanner | null, catalogEntries?: CatalogEntryRepository | null);
276
+ /**
277
+ * Discards a draft version. Allowed only while the version has not yet
278
+ * been published — published versions are preserved immutably,
279
+ * because existing subscriptions reference them (contract protection P1).
280
+ *
281
+ * If the repository does not implement the method, the endpoint
282
+ * responds with 501 Not Implemented (consumers without the editor stack
283
+ * should not register `PlanVersionsService` anyway).
284
+ */
285
+ discardPlanDraft(versionId: string): Promise<void>;
286
+ /**
287
+ * @param planUuid UUID of the plan root (Plan.id) — resolved to planKey
288
+ * before the repo call (lifecycle methods work with planKey because
289
+ * PlanVersion.planId is a string in the schema).
290
+ */
291
+ listPlanVersions(planUuid: string): Promise<PlanVersionRow[]>;
292
+ getPlanVersion(versionId: string): Promise<PlanVersionRow>;
293
+ /**
294
+ * Annotates each version with `isLatestInChain` (highest version number
295
+ * in the root) + `subscriptionCount` so that UI and backend make the same
296
+ * editability decision. `subscriptionCount` stays
297
+ * `undefined` when no SubscriptionRepository is registered —
298
+ * `isVersionEditable` interprets that fail-closed (= frozen).
299
+ */
300
+ private annotateEditability;
301
+ /**
302
+ * @param data.planId — the plan UUID from the controller. Resolved to
303
+ * planKey before the repo call.
304
+ */
305
+ createPlanDraft(data: CreatePlanVersionDraftData): Promise<PlanVersionMutationResult>;
306
+ updatePlanDraft(versionId: string, data: UpdatePlanVersionDraftData): Promise<PlanVersionMutationResult>;
307
+ publishPlanVersion(versionId: string, publishMeta: PublishPlanVersionData): Promise<PlanVersionMutationResult>;
308
+ /**
309
+ * Terminates a **live** PlanVersion by setting `endsAt`.
310
+ * Unlike auto-succession (validUntil), this is an explicit
311
+ * user action: the version is not replaced by a successor version
312
+ * but expires on the chosen date. Existing subscriptions
313
+ * (P1) stay bound — this only affects new bookings.
314
+ *
315
+ * Idempotent — a second call with a different date overwrites.
316
+ *
317
+ * Preconditions:
318
+ * - Version must be live (`publishedAt != null && supersededAt == null`).
319
+ * - `endsAt` must lie in the future (`endsAt > now`).
320
+ *
321
+ * If the repository does not implement the method, the endpoint
322
+ * responds with 422 `PLAN_TERMINATE_NOT_IMPLEMENTED`.
323
+ */
324
+ terminatePlanVersion(versionId: string, endsAt: Date): Promise<PlanVersionRow>;
325
+ private runStrictCheck;
326
+ private gateOrPass;
327
+ }
328
+
329
+ export { BundlesService as B, CatalogEntriesService as C, MarketingProjectionsService as M, PlanVersionsService as P, type CatalogServiceConfig as a, MarketingSettingsService as b, PlansService as c, PromotionsService as d, PublicMarketingCatalogService as e };
@@ -0,0 +1,329 @@
1
+ import { BundleRepository, DiscoverySnapshot, SubscriptionRepository, PlanRepository, CatalogEntryRepository, BundleRow, BundleVersionRow, CreateBundleData, UpdateBundleData, CreateBundleVersionDraftData, BundleVersionMutationResult, UpdateBundleVersionDraftData, PublishBundleVersionData, MarketingProjectionRepository, MarketingProjectionFilter, MarketingProjectionRow, CreateMarketingProjectionData, UpdateMarketingProjectionData, FeatureUiRegistry, CapabilityCatalogEntryRow, FeatureCatalogEntryRow, QuotaCatalogEntryRow, ReviewCatalogEntryData, CatalogEntryI18n, UpdateCatalogEntryBaseData, SyncDiscoveryResult, PromotionRepository, PromotionRow, CreatePromotionData, UpdatePromotionData, MarketingSettingsRepository, MarketingSettingsRow, UpdateMarketingSettingsData, PublicMarketingCatalogResponse, PlanRow, CreatePlanData, UpdatePlanData, PlanVersionRow, CreatePlanVersionDraftData, PlanVersionMutationResult, UpdatePlanVersionDraftData, PublishPlanVersionData } from '@saasicat/types';
2
+ import { OnApplicationBootstrap } from '@nestjs/common';
3
+ import { c as DiscoveryScanner } from './discovery.scanner-DLgEvuoy.cjs';
4
+
5
+ /**
6
+ * Configuration for the mutating operations.
7
+ */
8
+ interface CatalogServiceConfig {
9
+ /** Active mode for the strict-mode check. Default `'blocking'` (#12). */
10
+ strictModeCheckMode?: 'warn-only' | 'blocking';
11
+ /**
12
+ * Discovery→Catalog auto-sync at boot (#12). Default `true` — runs when a
13
+ * `DiscoverySnapshot` is available. Set to `false` to disable the boot
14
+ * sync; the manual `POST /admin/catalog/discovery/sync` is unaffected.
15
+ */
16
+ autoSyncDiscoveryAtBoot?: boolean;
17
+ /**
18
+ * Feature keys the catalog is deliberately allowed to carry WITHOUT them
19
+ * existing in the discovery snapshot (= in code via @ImplementsCapability) —
20
+ * marketed non-code features such as support SLAs (e.g. PRIORITY_SUPPORT).
21
+ * The strict-mode check does NOT report them as BUNDLE_/PLAN_FEATURE_UNKNOWN.
22
+ * Use sparingly: this is the only legitimate exception to "code is the
23
+ * source of truth"; not-yet-built features do NOT belong here but should be
24
+ * removed from the catalog until they are implemented.
25
+ */
26
+ marketedOnlyFeatures?: string[];
27
+ }
28
+ declare class BundlesService {
29
+ private readonly repo;
30
+ private readonly snapshot;
31
+ private readonly subscriptions;
32
+ private readonly planRepo;
33
+ private readonly scanner;
34
+ private readonly catalogEntries;
35
+ private readonly logger;
36
+ private mode;
37
+ private readonly marketedOnly;
38
+ constructor(repo: BundleRepository, snapshot?: DiscoverySnapshot | null, config?: CatalogServiceConfig, subscriptions?: SubscriptionRepository | null, planRepo?: PlanRepository | null, scanner?: DiscoveryScanner | null, catalogEntries?: CatalogEntryRepository | null);
39
+ listBundles(projectKey: string): Promise<BundleRow[]>;
40
+ getBundle(bundleId: string): Promise<{
41
+ bundle: BundleRow;
42
+ versions: BundleVersionRow[];
43
+ }>;
44
+ createBundle(data: CreateBundleData): Promise<BundleRow>;
45
+ updateBundle(bundleId: string, data: UpdateBundleData): Promise<BundleRow>;
46
+ softDeleteBundle(bundleId: string): Promise<void>;
47
+ /**
48
+ * Hard-deletes a draft BundleVersion (`publishedAt === null`) from the
49
+ * DB. Published versions stay immutable (contract-protection P1) — the
50
+ * service throws 422 with code `BUNDLE_VERSION_ALREADY_PUBLISHED`.
51
+ */
52
+ discardBundleDraft(versionId: string): Promise<void>;
53
+ listBundleVersions(bundleId: string): Promise<BundleVersionRow[]>;
54
+ getBundleVersion(versionId: string): Promise<BundleVersionRow>;
55
+ /**
56
+ * Annotates each BundleVersion with `isLatestInChain` (highest version
57
+ * number in the chain) + `subscriptionCount` so that UI and backend make
58
+ * the same editability decision. `subscriptionCount` stays `undefined`
59
+ * when no SubscriptionRepository is registered or the method was not
60
+ * implemented — `isVersionEditable` interprets that fail-closed
61
+ * (= frozen).
62
+ */
63
+ private annotateEditability;
64
+ /**
65
+ * Creates a new draft BundleVersion. Throws when a draft already exists
66
+ * for the same bundle (repository constraint).
67
+ *
68
+ * The strict-mode check runs on the input; in `warn-only` warnings are
69
+ * returned, in `blocking` the service throws HTTP 422.
70
+ */
71
+ createBundleDraft(data: CreateBundleVersionDraftData): Promise<BundleVersionMutationResult>;
72
+ /**
73
+ * Updates a draft version or — as a deliberate relaxation of
74
+ * contract-protection P1 — a published-but-future BundleVersion that is
75
+ * latest-in-chain and does not yet bind any subscription. Once
76
+ * `validFrom` is reached or a booking references it, the version freezes
77
+ * again. The concrete decision is made by the `isVersionEditable` helper,
78
+ * which is mirrored by the UI.
79
+ */
80
+ updateBundleDraft(versionId: string, data: UpdateBundleVersionDraftData): Promise<BundleVersionMutationResult>;
81
+ /**
82
+ * Publishes a draft version atomically. Steps:
83
+ * 1. Strict check on the draft definition (warn-only or blocking)
84
+ * 2. Enforce validFrom requirement + ordering against the predecessor
85
+ * (SPEC_V2 §4.2 + §11.1 M6 Pack 2c, analogous to PlanVersion)
86
+ * 3. Compute the diff against the predecessor version (latest live) →
87
+ * `publishedChanges` + `nonRegressive` via `classifyBundleVersionDiff`
88
+ * 4. Call Repository.publishDraft with the diff result + validity dates
89
+ * (transaction; auto-succession sets the previous validUntil)
90
+ */
91
+ publishBundleVersion(versionId: string, publishMeta: PublishBundleVersionData): Promise<BundleVersionMutationResult>;
92
+ private runStrictCheck;
93
+ private gateOrPass;
94
+ private assertBundleVersionWindowUpdate;
95
+ private findPreviousBundleVersion;
96
+ private parseOptionalBundleDate;
97
+ private parseRequiredBundleDate;
98
+ }
99
+
100
+ declare class MarketingProjectionsService {
101
+ private readonly repo;
102
+ constructor(repo: MarketingProjectionRepository);
103
+ list(filter: MarketingProjectionFilter): Promise<MarketingProjectionRow[]>;
104
+ getById(id: string): Promise<MarketingProjectionRow>;
105
+ create(data: CreateMarketingProjectionData): Promise<MarketingProjectionRow>;
106
+ update(id: string, data: UpdateMarketingProjectionData): Promise<MarketingProjectionRow>;
107
+ delete(id: string): Promise<void>;
108
+ }
109
+
110
+ declare class CatalogEntriesService implements OnApplicationBootstrap {
111
+ private readonly repo;
112
+ private readonly snapshot;
113
+ private readonly scanner;
114
+ private readonly featureUiRegistry;
115
+ private readonly logger;
116
+ private readonly autoSyncAtBoot;
117
+ constructor(repo: CatalogEntryRepository, snapshot?: DiscoverySnapshot | null, config?: CatalogServiceConfig, scanner?: DiscoveryScanner | null, featureUiRegistry?: FeatureUiRegistry | null);
118
+ /**
119
+ * Mirrors the discovery snapshot into the catalog entries at boot
120
+ * (discovery-as-SSOT, #12). Runs only when a snapshot is available and
121
+ * the kill switch `autoSyncDiscoveryAtBoot` is not set to `false`. A
122
+ * sync error deliberately does not abort the boot — the manual endpoint
123
+ * `POST /admin/catalog/discovery/sync` remains as a fallback.
124
+ */
125
+ onApplicationBootstrap(): Promise<void>;
126
+ listCapabilities(projectKey: string, codeStatus?: string): Promise<CapabilityCatalogEntryRow[]>;
127
+ listFeatures(projectKey: string, discoveryStatus?: string): Promise<FeatureCatalogEntryRow[]>;
128
+ listQuotas(projectKey: string, discoveryStatus?: string): Promise<QuotaCatalogEntryRow[]>;
129
+ /**
130
+ * Approval transition of a feature (#20). Validates the state machine;
131
+ * `approved` persists the approval signature from the current
132
+ * snapshot, `pending` (revoke/reactivate) deletes it, `outdated`/
133
+ * `obsolete` keep the last approval as history.
134
+ */
135
+ reviewFeature(projectKey: string, featureKey: string, data: ReviewCatalogEntryData, reviewedBy: string | null): Promise<FeatureCatalogEntryRow>;
136
+ /** Approval transition of a quota — same state machine as features (#20). */
137
+ reviewQuota(projectKey: string, quotaKey: string, data: ReviewCatalogEntryData, reviewedBy: string | null): Promise<QuotaCatalogEntryRow>;
138
+ private resolveReviewUpdate;
139
+ setFeatureI18n(projectKey: string, featureKey: string, i18n: CatalogEntryI18n): Promise<FeatureCatalogEntryRow>;
140
+ setQuotaI18n(projectKey: string, quotaKey: string, i18n: CatalogEntryI18n): Promise<QuotaCatalogEntryRow>;
141
+ /** Sets the editable default-locale label/description of a feature. */
142
+ setFeatureBase(projectKey: string, featureKey: string, data: UpdateCatalogEntryBaseData): Promise<FeatureCatalogEntryRow>;
143
+ /** Sets the editable default-locale label/description of a quota. */
144
+ setQuotaBase(projectKey: string, quotaKey: string, data: UpdateCatalogEntryBaseData): Promise<QuotaCatalogEntryRow>;
145
+ /**
146
+ * Synchronizes the catalog entries from a discovery snapshot.
147
+ * Idempotent — can run against the same snapshot any number of times.
148
+ */
149
+ syncFromSnapshot(snapshot: DiscoverySnapshot): Promise<SyncDiscoveryResult>;
150
+ /**
151
+ * `replaced` semantics (#39): if a key disappears from the snapshot AND
152
+ * another snapshot key claims it via `replaces`, the old entry gets
153
+ * the successor pointer — `obsolete` (from `retireMissing`)
154
+ * + `successorKey` instead of a bare `obsolete`. If a key reappears in
155
+ * the code, an existing pointer is deleted. A missing key
156
+ * without a claimant remains untouched (deleted without replacement).
157
+ */
158
+ private applySuccessorPointers;
159
+ }
160
+
161
+ declare class PromotionsService {
162
+ private readonly repo;
163
+ constructor(repo: PromotionRepository);
164
+ list(projectKey: string): Promise<PromotionRow[]>;
165
+ getById(id: string): Promise<PromotionRow>;
166
+ create(data: CreatePromotionData): Promise<PromotionRow>;
167
+ update(id: string, data: UpdatePromotionData): Promise<PromotionRow>;
168
+ delete(id: string): Promise<void>;
169
+ }
170
+
171
+ declare class MarketingSettingsService {
172
+ private readonly repo;
173
+ constructor(repo: MarketingSettingsRepository);
174
+ /** Returns the stored config or `null` (in which case the full pool applies). */
175
+ get(projectKey: string): Promise<MarketingSettingsRow | null>;
176
+ upsert(projectKey: string, data: UpdateMarketingSettingsData): Promise<MarketingSettingsRow>;
177
+ }
178
+
179
+ declare class PublicMarketingCatalogService {
180
+ private readonly planRepo;
181
+ private readonly marketingRepo;
182
+ private readonly promotionRepo;
183
+ private readonly catalogEntryRepo;
184
+ private readonly bundleRepo;
185
+ constructor(planRepo: PlanRepository, marketingRepo: MarketingProjectionRepository, promotionRepo: PromotionRepository, catalogEntryRepo?: CatalogEntryRepository | null, bundleRepo?: BundleRepository | null);
186
+ getCatalog(projectKey: string, locale: string, currency: string, vatRate: number, asOf?: Date): Promise<PublicMarketingCatalogResponse>;
187
+ /**
188
+ * Loads all published-and-live bundle versions for a project and
189
+ * filters on `marketed === true`. Without a registered
190
+ * `BundleRepository` (apps without the SuperAdmin bundle editor) the
191
+ * list is empty — the public catalog stays plan-only.
192
+ */
193
+ private loadMarketedBundles;
194
+ /**
195
+ * Maps BundleVersionRow → PublicMarketingBundle. Locale resolution
196
+ * runs via `MarketingProjectionRepository.findByTarget('BUNDLE',
197
+ * bundleVersionId, locale)` — on a miss it falls back to
198
+ * `DEFAULT_LOCALE`. If the fallback is missing too, label/description
199
+ * come directly from the bundle root (denormalized in `BundleVersionRow`).
200
+ *
201
+ * If the marketing projection is `visible === false`, the bundle is
202
+ * hidden from the public response (preparation phase).
203
+ */
204
+ private toPublicBundle;
205
+ /**
206
+ * Labels for bundle features ∪ their requires (#48): `comparison.features`
207
+ * only covers the plan-feature union — bundle-only features would
208
+ * otherwise get no label in the public payload. Curated keys only;
209
+ * without a CatalogEntryRepository the map stays empty (frontends fall
210
+ * back to the key).
211
+ */
212
+ private resolveBundleFeatureLabels;
213
+ /** Loads the translated feature/quota labels (+ sortOrder) for a locale. */
214
+ private loadLabelMeta;
215
+ /**
216
+ * Resolves the displayed label of a top feature: the `label` override
217
+ * wins; otherwise the translated label of the referenced `key`.
218
+ */
219
+ private resolveTopFeature;
220
+ /** Row definitions of the comparison matrix (feature/quota union + labels). */
221
+ private buildComparison;
222
+ private resolvePromo;
223
+ }
224
+
225
+ declare class PlansService {
226
+ private readonly repo;
227
+ private readonly subscriptions;
228
+ constructor(repo: PlanRepository, subscriptions?: SubscriptionRepository | null);
229
+ /**
230
+ * Platform-wide count of active (ACTIVE/TRIAL) subscriptions per `planKey`,
231
+ * across versions and across all tenants (RLS-exempt). Plans without a
232
+ * subscription are absent from the map. Returns `{}` when no
233
+ * `SubscriptionRepository` is registered or it does not implement
234
+ * `countActiveByPlanKey`.
235
+ */
236
+ getTenantCounts(projectKey: string): Promise<Record<string, number>>;
237
+ listPlans(projectKey: string, opts?: {
238
+ onlyPublished?: boolean;
239
+ }): Promise<PlanRow[]>;
240
+ getPlan(planId: string): Promise<PlanRow>;
241
+ createPlan(data: CreatePlanData): Promise<PlanRow>;
242
+ updatePlan(planId: string, data: UpdatePlanData): Promise<PlanRow>;
243
+ softDeletePlan(planId: string): Promise<void>;
244
+ /**
245
+ * Hard-deletes the plan master record. Only allowed for plans without
246
+ * `PlanVersion` entries — published versions are immutable under
247
+ * contract protection P1 (existing subscriptions reference them). The
248
+ * caller must discard drafts beforehand via `DELETE plan-versions/:id`.
249
+ *
250
+ * If the repository does not implement `hardDelete`, the endpoint
251
+ * responds with 422 `PLAN_HARD_DELETE_NOT_IMPLEMENTED` — consumers can
252
+ * then fall back to `softDelete`.
253
+ */
254
+ hardDeletePlan(planId: string): Promise<void>;
255
+ /**
256
+ * Platform invariant (contract protection P1): once a plan has ever had a
257
+ * published version, its plan master record is retained — regardless of
258
+ * whether the version has since been superseded. Subscriptions still
259
+ * reference it via the version, and the audit log must be able to resolve
260
+ * the plan.
261
+ */
262
+ private assertNoPublishedVersions;
263
+ private assertNoDrafts;
264
+ }
265
+
266
+ declare class PlanVersionsService {
267
+ private readonly repo;
268
+ private readonly snapshot;
269
+ private readonly subscriptions;
270
+ private readonly scanner;
271
+ private readonly catalogEntries;
272
+ private readonly logger;
273
+ private mode;
274
+ private readonly marketedOnly;
275
+ constructor(repo: PlanRepository, snapshot?: DiscoverySnapshot | null, config?: CatalogServiceConfig, subscriptions?: SubscriptionRepository | null, scanner?: DiscoveryScanner | null, catalogEntries?: CatalogEntryRepository | null);
276
+ /**
277
+ * Discards a draft version. Allowed only while the version has not yet
278
+ * been published — published versions are preserved immutably,
279
+ * because existing subscriptions reference them (contract protection P1).
280
+ *
281
+ * If the repository does not implement the method, the endpoint
282
+ * responds with 501 Not Implemented (consumers without the editor stack
283
+ * should not register `PlanVersionsService` anyway).
284
+ */
285
+ discardPlanDraft(versionId: string): Promise<void>;
286
+ /**
287
+ * @param planUuid UUID of the plan root (Plan.id) — resolved to planKey
288
+ * before the repo call (lifecycle methods work with planKey because
289
+ * PlanVersion.planId is a string in the schema).
290
+ */
291
+ listPlanVersions(planUuid: string): Promise<PlanVersionRow[]>;
292
+ getPlanVersion(versionId: string): Promise<PlanVersionRow>;
293
+ /**
294
+ * Annotates each version with `isLatestInChain` (highest version number
295
+ * in the root) + `subscriptionCount` so that UI and backend make the same
296
+ * editability decision. `subscriptionCount` stays
297
+ * `undefined` when no SubscriptionRepository is registered —
298
+ * `isVersionEditable` interprets that fail-closed (= frozen).
299
+ */
300
+ private annotateEditability;
301
+ /**
302
+ * @param data.planId — the plan UUID from the controller. Resolved to
303
+ * planKey before the repo call.
304
+ */
305
+ createPlanDraft(data: CreatePlanVersionDraftData): Promise<PlanVersionMutationResult>;
306
+ updatePlanDraft(versionId: string, data: UpdatePlanVersionDraftData): Promise<PlanVersionMutationResult>;
307
+ publishPlanVersion(versionId: string, publishMeta: PublishPlanVersionData): Promise<PlanVersionMutationResult>;
308
+ /**
309
+ * Terminates a **live** PlanVersion by setting `endsAt`.
310
+ * Unlike auto-succession (validUntil), this is an explicit
311
+ * user action: the version is not replaced by a successor version
312
+ * but expires on the chosen date. Existing subscriptions
313
+ * (P1) stay bound — this only affects new bookings.
314
+ *
315
+ * Idempotent — a second call with a different date overwrites.
316
+ *
317
+ * Preconditions:
318
+ * - Version must be live (`publishedAt != null && supersededAt == null`).
319
+ * - `endsAt` must lie in the future (`endsAt > now`).
320
+ *
321
+ * If the repository does not implement the method, the endpoint
322
+ * responds with 422 `PLAN_TERMINATE_NOT_IMPLEMENTED`.
323
+ */
324
+ terminatePlanVersion(versionId: string, endsAt: Date): Promise<PlanVersionRow>;
325
+ private runStrictCheck;
326
+ private gateOrPass;
327
+ }
328
+
329
+ export { BundlesService as B, CatalogEntriesService as C, MarketingProjectionsService as M, PlanVersionsService as P, type CatalogServiceConfig as a, MarketingSettingsService as b, PlansService as c, PromotionsService as d, PublicMarketingCatalogService as e };