@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,180 @@
|
|
|
1
|
+
import { Type, CanActivate, DynamicModule, ForwardReference, FactoryProvider } from '@nestjs/common';
|
|
2
|
+
import { MfaPort, AuditPort, RlsBypassPort, PlanCatalogReadSink, SubscriptionRepository, PlanVersionRepository, TransactionRunner, PlanCatalog, QuotaProvider } from '@saasicat/types';
|
|
3
|
+
import { P as ProviderSpec } from './di-CcNeq9v-.cjs';
|
|
4
|
+
import { A as AdminManifestConfig } from './admin-manifest.config-DyrQNT7M.cjs';
|
|
5
|
+
import { D as DiscoveryAppInfo } from './discovery.scanner-CUYLKlYT.cjs';
|
|
6
|
+
import { E as EntitlementResolutionConfig } from './plan-resolution-CFCoUkrE.cjs';
|
|
7
|
+
|
|
8
|
+
declare const PLAN_RESOLVER_PORT_TOKEN: unique symbol;
|
|
9
|
+
interface PlanResolverPort {
|
|
10
|
+
/**
|
|
11
|
+
* Returns the tenant's active `planId`. `null` = no plan assigned →
|
|
12
|
+
* the guard / interceptor throws 403 or lets it through, depending on
|
|
13
|
+
* the configuration.
|
|
14
|
+
*/
|
|
15
|
+
getPlanIdForTenant(tenantId: string): Promise<string | null>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Static fallback: always returns the same plan for all tenants. Useful
|
|
19
|
+
* for local development and for apps that do not yet have a contract concept.
|
|
20
|
+
*
|
|
21
|
+
* The quickstart path uses this implicitly when `adapters.planResolver` is
|
|
22
|
+
* not set and `defaultPlanId` was provided in SaasPlatformModule.forRoot().
|
|
23
|
+
*/
|
|
24
|
+
declare class StaticPlanResolver implements PlanResolverPort {
|
|
25
|
+
private readonly planId;
|
|
26
|
+
constructor(planId: string);
|
|
27
|
+
getPlanIdForTenant(): Promise<string | null>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface TenantManifestControllerOptions {
|
|
31
|
+
path?: string;
|
|
32
|
+
guards: Array<Type<CanActivate>>;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Creates a controller class that maps `GET <path>` to
|
|
36
|
+
* `TenantManifestService.getManifest(tenantId)`. `tenantId` comes from
|
|
37
|
+
* `request.user.tenantId` (default — matches the JwtAuthGuard pattern of
|
|
38
|
+
* most apps) or from `request.tenantId`.
|
|
39
|
+
*
|
|
40
|
+
* Route path: default `tenant/manifest`. Apps with a globalPrefix `/api/v1`
|
|
41
|
+
* thus get `/api/v1/tenant/manifest`.
|
|
42
|
+
*/
|
|
43
|
+
declare function buildTenantManifestController(options: TenantManifestControllerOptions): Type<unknown>;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Adapter bindings for the platform ports. Accepted as class tokens, values
|
|
47
|
+
* or factory specs.
|
|
48
|
+
*/
|
|
49
|
+
interface SaasPlatformAdapters {
|
|
50
|
+
mfa: ProviderSpec<MfaPort>;
|
|
51
|
+
audit: ProviderSpec<AuditPort>;
|
|
52
|
+
rlsBypass: ProviderSpec<RlsBypassPort>;
|
|
53
|
+
/**
|
|
54
|
+
* Optional. If provided, `PlanCatalogModule` is hydrated from this sink
|
|
55
|
+
* (DB read at boot). If omitted, `planCatalog` MUST be passed as a ready
|
|
56
|
+
* object (quickstart path — YAML-direct).
|
|
57
|
+
*/
|
|
58
|
+
planCatalogReadSink?: ProviderSpec<PlanCatalogReadSink>;
|
|
59
|
+
/**
|
|
60
|
+
* Optional — resolver `tenantId → planId`. The quickstart path uses this
|
|
61
|
+
* together with the `StaticEntitlementService` to automatically check
|
|
62
|
+
* `@RequireFeature` and `@EnforceQuota` against the plan catalog limit.
|
|
63
|
+
* If not set, `defaultPlanId` **must** be provided — a `StaticPlanResolver`
|
|
64
|
+
* then returns the same plan for all tenants.
|
|
65
|
+
*/
|
|
66
|
+
planResolver?: ProviderSpec<PlanResolverPort>;
|
|
67
|
+
/**
|
|
68
|
+
* Optional — required only when `entitlement: true`. Repositories for the
|
|
69
|
+
* V3 contract/entitlement loop.
|
|
70
|
+
*/
|
|
71
|
+
subscriptionRepository?: ProviderSpec<SubscriptionRepository>;
|
|
72
|
+
planVersionRepository?: ProviderSpec<PlanVersionRepository>;
|
|
73
|
+
transactionRunner?: ProviderSpec<TransactionRunner>;
|
|
74
|
+
}
|
|
75
|
+
interface SaasPlatformModuleOptions {
|
|
76
|
+
/**
|
|
77
|
+
* Plan catalog. Either as an already-loaded object (quickstart, comes
|
|
78
|
+
* directly from `loadPlanCatalogFromFile('config/saas.yaml')`) or as a
|
|
79
|
+
* sink reference in `adapters.planCatalogReadSink` for DB hydration.
|
|
80
|
+
*/
|
|
81
|
+
planCatalog?: PlanCatalog;
|
|
82
|
+
/**
|
|
83
|
+
* Adapter bindings.
|
|
84
|
+
*/
|
|
85
|
+
adapters: SaasPlatformAdapters;
|
|
86
|
+
/**
|
|
87
|
+
* Class-level guards for the platform controllers (`GET /admin/discovery`
|
|
88
|
+
* and `GET /admin/manifest`). REQUIRED — otherwise the platform throws at
|
|
89
|
+
* boot, because a manifest controller must never be silently registered
|
|
90
|
+
* without auth (platform security).
|
|
91
|
+
*
|
|
92
|
+
* Pass `[]` explicitly if the endpoint is intentionally auth-free
|
|
93
|
+
* (CI/smoke test).
|
|
94
|
+
*/
|
|
95
|
+
controller: {
|
|
96
|
+
guards: Array<Type<CanActivate>>;
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* Additional guards only for `POST /admin/manifest/reload` (typically:
|
|
100
|
+
* `MfaGuard`). Optional.
|
|
101
|
+
*/
|
|
102
|
+
reloadGuards?: Array<Type<CanActivate>>;
|
|
103
|
+
/**
|
|
104
|
+
* Modules whose providers must be visible in the DI scope (typically:
|
|
105
|
+
* `AuthModule` with the `JwtAuthGuard`).
|
|
106
|
+
*/
|
|
107
|
+
imports?: Array<Type<unknown> | DynamicModule | Promise<DynamicModule> | ForwardReference>;
|
|
108
|
+
/**
|
|
109
|
+
* App identity for the DiscoveryScanner. If omitted, `planCatalog.app` is
|
|
110
|
+
* used (recommendation: simply declare it in the YAML).
|
|
111
|
+
*/
|
|
112
|
+
app?: DiscoveryAppInfo;
|
|
113
|
+
/**
|
|
114
|
+
* Optional — snapshot path for the DiscoveryScanner. Default:
|
|
115
|
+
* `var/discovery-snapshot.json`. `null` to disable.
|
|
116
|
+
*/
|
|
117
|
+
discoverySnapshotPath?: string | null;
|
|
118
|
+
/**
|
|
119
|
+
* `AdminManifestConfig`. If omitted, the module assembles a minimal variant
|
|
120
|
+
* from `planCatalog` — good for quickstart, but for full manifest features
|
|
121
|
+
* (build hash, locales, KPI cards) the consumer should provide its own
|
|
122
|
+
* factory.
|
|
123
|
+
*/
|
|
124
|
+
adminManifestConfig?: AdminManifestConfig | Pick<FactoryProvider<AdminManifestConfig>, 'useFactory' | 'inject'>;
|
|
125
|
+
/**
|
|
126
|
+
* Default `false`. If `true`, `EntitlementModule.forRoot({...})` is called
|
|
127
|
+
* with the repositories from `adapters` — only meaningful if the app
|
|
128
|
+
* implements the V3 contract path (`subscriptionRepository` & co. must then
|
|
129
|
+
* be set).
|
|
130
|
+
*/
|
|
131
|
+
entitlement?: false | {
|
|
132
|
+
resolutionConfig?: EntitlementResolutionConfig;
|
|
133
|
+
};
|
|
134
|
+
/**
|
|
135
|
+
* Fallback plan ID for the `StaticPlanResolver`. If neither
|
|
136
|
+
* `adapters.planResolver` nor `defaultPlanId` is set, the
|
|
137
|
+
* `StaticEntitlementService` is not activated — `@RequireFeature`/
|
|
138
|
+
* `@EnforceQuota` are then **ineffective** (discovery markup with no
|
|
139
|
+
* runtime effect). Platform warning at boot.
|
|
140
|
+
*/
|
|
141
|
+
defaultPlanId?: string;
|
|
142
|
+
/**
|
|
143
|
+
* QuotaProvider classes declared with `@DefinesQuota({...})` that the
|
|
144
|
+
* `EnforceQuotaInterceptor` must use for count calculation. The platform
|
|
145
|
+
* registers them as app providers and collects them in
|
|
146
|
+
* `QUOTA_PROVIDERS_TOKEN`.
|
|
147
|
+
*/
|
|
148
|
+
quotaProviders?: Array<Type<QuotaProvider>>;
|
|
149
|
+
/**
|
|
150
|
+
* Enable the tenant manifest — the app UI gets a filtered manifest per
|
|
151
|
+
* tenant with features, quotas and visible navigation. Requires that
|
|
152
|
+
* `defaultPlanId` or `adapters.planResolver` is set.
|
|
153
|
+
*/
|
|
154
|
+
tenantManifest?: TenantManifestControllerOptions;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Bundles PlanCatalog + Discovery + Admin + AdminManifest (+ optionally
|
|
158
|
+
* Entitlement) into a single `forRoot({...})` call. Reduces AppModule
|
|
159
|
+
* boilerplate and eliminates the ordering trap.
|
|
160
|
+
*
|
|
161
|
+
* Quickstart path:
|
|
162
|
+
*
|
|
163
|
+
* ```ts
|
|
164
|
+
* SaasPlatformModule.forRoot({
|
|
165
|
+
* planCatalog: loadPlanCatalogFromFile({ path: 'config/saas.yaml' }),
|
|
166
|
+
* controller: { guards: [JwtAuthGuard] },
|
|
167
|
+
* imports: [AuthModule],
|
|
168
|
+
* adapters: {
|
|
169
|
+
* mfa: PrismaMfaAdapter, // from @saasicat/prisma
|
|
170
|
+
* audit: PrismaAuditAdapter,
|
|
171
|
+
* rlsBypass: AsyncLocalRlsBypassAdapter,
|
|
172
|
+
* },
|
|
173
|
+
* })
|
|
174
|
+
* ```
|
|
175
|
+
*/
|
|
176
|
+
declare class SaasPlatformModule {
|
|
177
|
+
static forRoot(options: SaasPlatformModuleOptions): DynamicModule;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export { type PlanResolverPort as P, type SaasPlatformModuleOptions as S, type TenantManifestControllerOptions as T, PLAN_RESOLVER_PORT_TOKEN as a, type SaasPlatformAdapters as b, SaasPlatformModule as c, StaticPlanResolver as d, buildTenantManifestController as e };
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { Type, CanActivate, DynamicModule, ForwardReference, FactoryProvider } from '@nestjs/common';
|
|
2
|
+
import { MfaPort, AuditPort, RlsBypassPort, PlanCatalogReadSink, SubscriptionRepository, PlanVersionRepository, TransactionRunner, PlanCatalog, QuotaProvider } from '@saasicat/types';
|
|
3
|
+
import { P as ProviderSpec } from './di-CcNeq9v-.js';
|
|
4
|
+
import { A as AdminManifestConfig } from './admin-manifest.config-DyrQNT7M.js';
|
|
5
|
+
import { D as DiscoveryAppInfo } from './discovery.scanner-CUYLKlYT.js';
|
|
6
|
+
import { E as EntitlementResolutionConfig } from './plan-resolution-CFCoUkrE.js';
|
|
7
|
+
|
|
8
|
+
declare const PLAN_RESOLVER_PORT_TOKEN: unique symbol;
|
|
9
|
+
interface PlanResolverPort {
|
|
10
|
+
/**
|
|
11
|
+
* Returns the tenant's active `planId`. `null` = no plan assigned →
|
|
12
|
+
* the guard / interceptor throws 403 or lets it through, depending on
|
|
13
|
+
* the configuration.
|
|
14
|
+
*/
|
|
15
|
+
getPlanIdForTenant(tenantId: string): Promise<string | null>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Static fallback: always returns the same plan for all tenants. Useful
|
|
19
|
+
* for local development and for apps that do not yet have a contract concept.
|
|
20
|
+
*
|
|
21
|
+
* The quickstart path uses this implicitly when `adapters.planResolver` is
|
|
22
|
+
* not set and `defaultPlanId` was provided in SaasPlatformModule.forRoot().
|
|
23
|
+
*/
|
|
24
|
+
declare class StaticPlanResolver implements PlanResolverPort {
|
|
25
|
+
private readonly planId;
|
|
26
|
+
constructor(planId: string);
|
|
27
|
+
getPlanIdForTenant(): Promise<string | null>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface TenantManifestControllerOptions {
|
|
31
|
+
path?: string;
|
|
32
|
+
guards: Array<Type<CanActivate>>;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Creates a controller class that maps `GET <path>` to
|
|
36
|
+
* `TenantManifestService.getManifest(tenantId)`. `tenantId` comes from
|
|
37
|
+
* `request.user.tenantId` (default — matches the JwtAuthGuard pattern of
|
|
38
|
+
* most apps) or from `request.tenantId`.
|
|
39
|
+
*
|
|
40
|
+
* Route path: default `tenant/manifest`. Apps with a globalPrefix `/api/v1`
|
|
41
|
+
* thus get `/api/v1/tenant/manifest`.
|
|
42
|
+
*/
|
|
43
|
+
declare function buildTenantManifestController(options: TenantManifestControllerOptions): Type<unknown>;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Adapter bindings for the platform ports. Accepted as class tokens, values
|
|
47
|
+
* or factory specs.
|
|
48
|
+
*/
|
|
49
|
+
interface SaasPlatformAdapters {
|
|
50
|
+
mfa: ProviderSpec<MfaPort>;
|
|
51
|
+
audit: ProviderSpec<AuditPort>;
|
|
52
|
+
rlsBypass: ProviderSpec<RlsBypassPort>;
|
|
53
|
+
/**
|
|
54
|
+
* Optional. If provided, `PlanCatalogModule` is hydrated from this sink
|
|
55
|
+
* (DB read at boot). If omitted, `planCatalog` MUST be passed as a ready
|
|
56
|
+
* object (quickstart path — YAML-direct).
|
|
57
|
+
*/
|
|
58
|
+
planCatalogReadSink?: ProviderSpec<PlanCatalogReadSink>;
|
|
59
|
+
/**
|
|
60
|
+
* Optional — resolver `tenantId → planId`. The quickstart path uses this
|
|
61
|
+
* together with the `StaticEntitlementService` to automatically check
|
|
62
|
+
* `@RequireFeature` and `@EnforceQuota` against the plan catalog limit.
|
|
63
|
+
* If not set, `defaultPlanId` **must** be provided — a `StaticPlanResolver`
|
|
64
|
+
* then returns the same plan for all tenants.
|
|
65
|
+
*/
|
|
66
|
+
planResolver?: ProviderSpec<PlanResolverPort>;
|
|
67
|
+
/**
|
|
68
|
+
* Optional — required only when `entitlement: true`. Repositories for the
|
|
69
|
+
* V3 contract/entitlement loop.
|
|
70
|
+
*/
|
|
71
|
+
subscriptionRepository?: ProviderSpec<SubscriptionRepository>;
|
|
72
|
+
planVersionRepository?: ProviderSpec<PlanVersionRepository>;
|
|
73
|
+
transactionRunner?: ProviderSpec<TransactionRunner>;
|
|
74
|
+
}
|
|
75
|
+
interface SaasPlatformModuleOptions {
|
|
76
|
+
/**
|
|
77
|
+
* Plan catalog. Either as an already-loaded object (quickstart, comes
|
|
78
|
+
* directly from `loadPlanCatalogFromFile('config/saas.yaml')`) or as a
|
|
79
|
+
* sink reference in `adapters.planCatalogReadSink` for DB hydration.
|
|
80
|
+
*/
|
|
81
|
+
planCatalog?: PlanCatalog;
|
|
82
|
+
/**
|
|
83
|
+
* Adapter bindings.
|
|
84
|
+
*/
|
|
85
|
+
adapters: SaasPlatformAdapters;
|
|
86
|
+
/**
|
|
87
|
+
* Class-level guards for the platform controllers (`GET /admin/discovery`
|
|
88
|
+
* and `GET /admin/manifest`). REQUIRED — otherwise the platform throws at
|
|
89
|
+
* boot, because a manifest controller must never be silently registered
|
|
90
|
+
* without auth (platform security).
|
|
91
|
+
*
|
|
92
|
+
* Pass `[]` explicitly if the endpoint is intentionally auth-free
|
|
93
|
+
* (CI/smoke test).
|
|
94
|
+
*/
|
|
95
|
+
controller: {
|
|
96
|
+
guards: Array<Type<CanActivate>>;
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* Additional guards only for `POST /admin/manifest/reload` (typically:
|
|
100
|
+
* `MfaGuard`). Optional.
|
|
101
|
+
*/
|
|
102
|
+
reloadGuards?: Array<Type<CanActivate>>;
|
|
103
|
+
/**
|
|
104
|
+
* Modules whose providers must be visible in the DI scope (typically:
|
|
105
|
+
* `AuthModule` with the `JwtAuthGuard`).
|
|
106
|
+
*/
|
|
107
|
+
imports?: Array<Type<unknown> | DynamicModule | Promise<DynamicModule> | ForwardReference>;
|
|
108
|
+
/**
|
|
109
|
+
* App identity for the DiscoveryScanner. If omitted, `planCatalog.app` is
|
|
110
|
+
* used (recommendation: simply declare it in the YAML).
|
|
111
|
+
*/
|
|
112
|
+
app?: DiscoveryAppInfo;
|
|
113
|
+
/**
|
|
114
|
+
* Optional — snapshot path for the DiscoveryScanner. Default:
|
|
115
|
+
* `var/discovery-snapshot.json`. `null` to disable.
|
|
116
|
+
*/
|
|
117
|
+
discoverySnapshotPath?: string | null;
|
|
118
|
+
/**
|
|
119
|
+
* `AdminManifestConfig`. If omitted, the module assembles a minimal variant
|
|
120
|
+
* from `planCatalog` — good for quickstart, but for full manifest features
|
|
121
|
+
* (build hash, locales, KPI cards) the consumer should provide its own
|
|
122
|
+
* factory.
|
|
123
|
+
*/
|
|
124
|
+
adminManifestConfig?: AdminManifestConfig | Pick<FactoryProvider<AdminManifestConfig>, 'useFactory' | 'inject'>;
|
|
125
|
+
/**
|
|
126
|
+
* Default `false`. If `true`, `EntitlementModule.forRoot({...})` is called
|
|
127
|
+
* with the repositories from `adapters` — only meaningful if the app
|
|
128
|
+
* implements the V3 contract path (`subscriptionRepository` & co. must then
|
|
129
|
+
* be set).
|
|
130
|
+
*/
|
|
131
|
+
entitlement?: false | {
|
|
132
|
+
resolutionConfig?: EntitlementResolutionConfig;
|
|
133
|
+
};
|
|
134
|
+
/**
|
|
135
|
+
* Fallback plan ID for the `StaticPlanResolver`. If neither
|
|
136
|
+
* `adapters.planResolver` nor `defaultPlanId` is set, the
|
|
137
|
+
* `StaticEntitlementService` is not activated — `@RequireFeature`/
|
|
138
|
+
* `@EnforceQuota` are then **ineffective** (discovery markup with no
|
|
139
|
+
* runtime effect). Platform warning at boot.
|
|
140
|
+
*/
|
|
141
|
+
defaultPlanId?: string;
|
|
142
|
+
/**
|
|
143
|
+
* QuotaProvider classes declared with `@DefinesQuota({...})` that the
|
|
144
|
+
* `EnforceQuotaInterceptor` must use for count calculation. The platform
|
|
145
|
+
* registers them as app providers and collects them in
|
|
146
|
+
* `QUOTA_PROVIDERS_TOKEN`.
|
|
147
|
+
*/
|
|
148
|
+
quotaProviders?: Array<Type<QuotaProvider>>;
|
|
149
|
+
/**
|
|
150
|
+
* Enable the tenant manifest — the app UI gets a filtered manifest per
|
|
151
|
+
* tenant with features, quotas and visible navigation. Requires that
|
|
152
|
+
* `defaultPlanId` or `adapters.planResolver` is set.
|
|
153
|
+
*/
|
|
154
|
+
tenantManifest?: TenantManifestControllerOptions;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Bundles PlanCatalog + Discovery + Admin + AdminManifest (+ optionally
|
|
158
|
+
* Entitlement) into a single `forRoot({...})` call. Reduces AppModule
|
|
159
|
+
* boilerplate and eliminates the ordering trap.
|
|
160
|
+
*
|
|
161
|
+
* Quickstart path:
|
|
162
|
+
*
|
|
163
|
+
* ```ts
|
|
164
|
+
* SaasPlatformModule.forRoot({
|
|
165
|
+
* planCatalog: loadPlanCatalogFromFile({ path: 'config/saas.yaml' }),
|
|
166
|
+
* controller: { guards: [JwtAuthGuard] },
|
|
167
|
+
* imports: [AuthModule],
|
|
168
|
+
* adapters: {
|
|
169
|
+
* mfa: PrismaMfaAdapter, // from @saasicat/prisma
|
|
170
|
+
* audit: PrismaAuditAdapter,
|
|
171
|
+
* rlsBypass: AsyncLocalRlsBypassAdapter,
|
|
172
|
+
* },
|
|
173
|
+
* })
|
|
174
|
+
* ```
|
|
175
|
+
*/
|
|
176
|
+
declare class SaasPlatformModule {
|
|
177
|
+
static forRoot(options: SaasPlatformModuleOptions): DynamicModule;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export { type PlanResolverPort as P, type SaasPlatformModuleOptions as S, type TenantManifestControllerOptions as T, PLAN_RESOLVER_PORT_TOKEN as a, type SaasPlatformAdapters as b, SaasPlatformModule as c, StaticPlanResolver as d, buildTenantManifestController as e };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { PromoCodeRepository, PromoCodeRedemptionRepository, PromoCodeValidationLogRepository, FirstTimeCustomerCheck, PromoSubscriptionLookup, PromoRevenueDeductionAggregator, TransactionRunner, PlanCatalog, CreatePromoCodeData, PromoCodeRecord, UpdatePromoCodeData, PromoCodeFilter, PromoCodeRedemptionListItem, BillingCycle, PromoCodeRedemptionRecord, TransactionContext } from '@saasicat/types';
|
|
2
|
+
|
|
3
|
+
type PreviewReason = 'NOT_FOUND' | 'EXPIRED' | 'EXHAUSTED' | 'PAUSED' | 'PLAN_MISMATCH' | 'BILLING_MISMATCH' | 'BELOW_MINIMUM_AMOUNT' | 'WOULD_PRODUCE_ZERO_INVOICE' | 'NOT_FIRST_TIME_CUSTOMER' | 'RATE_LIMITED';
|
|
4
|
+
type PreviewInvalid = {
|
|
5
|
+
valid: false;
|
|
6
|
+
reason: PreviewReason;
|
|
7
|
+
};
|
|
8
|
+
interface PreviewValid {
|
|
9
|
+
valid: true;
|
|
10
|
+
code: string;
|
|
11
|
+
label: string;
|
|
12
|
+
discount: {
|
|
13
|
+
valueType: PromoCodeRecord['valueType'];
|
|
14
|
+
value: string;
|
|
15
|
+
durationType: PromoCodeRecord['durationType'];
|
|
16
|
+
durationValue: number | null;
|
|
17
|
+
};
|
|
18
|
+
price: {
|
|
19
|
+
originalGross: string;
|
|
20
|
+
discountGross: string;
|
|
21
|
+
discountedGross: string;
|
|
22
|
+
includedVat: string;
|
|
23
|
+
nextRegularAmountGross: string;
|
|
24
|
+
regularStartsAt: string | null;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
type PreviewResult = PreviewValid | PreviewInvalid;
|
|
28
|
+
interface PreviewInput {
|
|
29
|
+
code: string;
|
|
30
|
+
planId: string;
|
|
31
|
+
billingCycle: BillingCycle;
|
|
32
|
+
/** For the firstTimeCustomersOnly check. */
|
|
33
|
+
email?: string;
|
|
34
|
+
ipHash?: string;
|
|
35
|
+
sessionId?: string;
|
|
36
|
+
}
|
|
37
|
+
interface RedeemInput {
|
|
38
|
+
code: string;
|
|
39
|
+
subscriptionId: string;
|
|
40
|
+
tenantId: string;
|
|
41
|
+
/** For the firstTimeCustomersOnly check in the final redeem path. */
|
|
42
|
+
email?: string;
|
|
43
|
+
}
|
|
44
|
+
interface PromoServiceConfig {
|
|
45
|
+
/** Plans that are not discountable (e.g. 'ENTERPRISE'). */
|
|
46
|
+
nonRedeemablePlans?: readonly string[];
|
|
47
|
+
}
|
|
48
|
+
interface PromoCodeStats {
|
|
49
|
+
code: PromoCodeRecord;
|
|
50
|
+
validations: number;
|
|
51
|
+
redemptions: {
|
|
52
|
+
active: number;
|
|
53
|
+
total: number;
|
|
54
|
+
reversed: number;
|
|
55
|
+
expired: number;
|
|
56
|
+
};
|
|
57
|
+
/** Decimal-as-string ('0.00' if no aggregator is available). */
|
|
58
|
+
revenueDeductionGross: string;
|
|
59
|
+
}
|
|
60
|
+
declare class PromoCodesService {
|
|
61
|
+
private readonly promoRepo;
|
|
62
|
+
private readonly redemptionRepo;
|
|
63
|
+
private readonly validationLogRepo;
|
|
64
|
+
private readonly firstTimeCheck;
|
|
65
|
+
private readonly subscriptionLookup;
|
|
66
|
+
private readonly revenueAggregator;
|
|
67
|
+
private readonly transactionRunner;
|
|
68
|
+
private readonly planCatalog;
|
|
69
|
+
private readonly config;
|
|
70
|
+
constructor(promoRepo: PromoCodeRepository, redemptionRepo: PromoCodeRedemptionRepository, validationLogRepo: PromoCodeValidationLogRepository, firstTimeCheck: FirstTimeCustomerCheck, subscriptionLookup: PromoSubscriptionLookup, revenueAggregator: PromoRevenueDeductionAggregator, transactionRunner: TransactionRunner, planCatalog: PlanCatalog, config: PromoServiceConfig);
|
|
71
|
+
create(input: CreatePromoCodeData): Promise<PromoCodeRecord>;
|
|
72
|
+
update(id: string, input: UpdatePromoCodeData): Promise<PromoCodeRecord>;
|
|
73
|
+
softDelete(id: string): Promise<void>;
|
|
74
|
+
findAll(filter?: PromoCodeFilter): Promise<PromoCodeRecord[]>;
|
|
75
|
+
findOne(id: string): Promise<PromoCodeRecord>;
|
|
76
|
+
stats(id: string): Promise<PromoCodeStats>;
|
|
77
|
+
listRedemptions(promoCodeId: string): Promise<PromoCodeRedemptionListItem[]>;
|
|
78
|
+
preview(input: PreviewInput): Promise<PreviewResult>;
|
|
79
|
+
/**
|
|
80
|
+
* Wraps `redeemInTransaction` in its own transaction-runner call —
|
|
81
|
+
* the default path when the caller has no external tx context
|
|
82
|
+
* (e.g. POST /billing/promo/redeem as a stand-alone endpoint).
|
|
83
|
+
*/
|
|
84
|
+
redeem(input: RedeemInput): Promise<PromoCodeRedemptionRecord>;
|
|
85
|
+
/**
|
|
86
|
+
* Redeems a promo code within an EXTERNAL transaction — the caller
|
|
87
|
+
* (typically `TenantSubscriptionWritePort.applyOnboardingSelection`)
|
|
88
|
+
* already has `prisma.$transaction(...)` open and passes the `tx`
|
|
89
|
+
* context through. This lands plan change, add-on insert, and redemption
|
|
90
|
+
* insert in a single DB transaction (P10.1.1).
|
|
91
|
+
*/
|
|
92
|
+
redeemInTransaction(input: RedeemInput, tx: TransactionContext): Promise<PromoCodeRedemptionRecord>;
|
|
93
|
+
reverse(subscriptionId: string): Promise<PromoCodeRedemptionRecord | null>;
|
|
94
|
+
private checkEligibility;
|
|
95
|
+
private checkFirstTimeCustomer;
|
|
96
|
+
/**
|
|
97
|
+
* Lazy expiry: sets codes whose validUntil has passed to EXPIRED.
|
|
98
|
+
* Called before every find/preview — defense-in-depth alongside the cron.
|
|
99
|
+
*/
|
|
100
|
+
private lazyExpire;
|
|
101
|
+
/**
|
|
102
|
+
* Lowest applicable plan price. With a whitelist it takes the minimum
|
|
103
|
+
* from the whitelist, otherwise across all marketed plans of the catalog
|
|
104
|
+
* (except non-redeemable).
|
|
105
|
+
*/
|
|
106
|
+
private lowestApplicablePlanGross;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export { type PreviewInput as P, type RedeemInput as R, type PreviewInvalid as a, type PreviewReason as b, type PreviewResult as c, type PreviewValid as d, type PromoCodeStats as e, PromoCodesService as f, type PromoServiceConfig as g };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { PromoCodeRepository, PromoCodeRedemptionRepository, PromoCodeValidationLogRepository, FirstTimeCustomerCheck, PromoSubscriptionLookup, PromoRevenueDeductionAggregator, TransactionRunner, PlanCatalog, CreatePromoCodeData, PromoCodeRecord, UpdatePromoCodeData, PromoCodeFilter, PromoCodeRedemptionListItem, BillingCycle, PromoCodeRedemptionRecord, TransactionContext } from '@saasicat/types';
|
|
2
|
+
|
|
3
|
+
type PreviewReason = 'NOT_FOUND' | 'EXPIRED' | 'EXHAUSTED' | 'PAUSED' | 'PLAN_MISMATCH' | 'BILLING_MISMATCH' | 'BELOW_MINIMUM_AMOUNT' | 'WOULD_PRODUCE_ZERO_INVOICE' | 'NOT_FIRST_TIME_CUSTOMER' | 'RATE_LIMITED';
|
|
4
|
+
type PreviewInvalid = {
|
|
5
|
+
valid: false;
|
|
6
|
+
reason: PreviewReason;
|
|
7
|
+
};
|
|
8
|
+
interface PreviewValid {
|
|
9
|
+
valid: true;
|
|
10
|
+
code: string;
|
|
11
|
+
label: string;
|
|
12
|
+
discount: {
|
|
13
|
+
valueType: PromoCodeRecord['valueType'];
|
|
14
|
+
value: string;
|
|
15
|
+
durationType: PromoCodeRecord['durationType'];
|
|
16
|
+
durationValue: number | null;
|
|
17
|
+
};
|
|
18
|
+
price: {
|
|
19
|
+
originalGross: string;
|
|
20
|
+
discountGross: string;
|
|
21
|
+
discountedGross: string;
|
|
22
|
+
includedVat: string;
|
|
23
|
+
nextRegularAmountGross: string;
|
|
24
|
+
regularStartsAt: string | null;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
type PreviewResult = PreviewValid | PreviewInvalid;
|
|
28
|
+
interface PreviewInput {
|
|
29
|
+
code: string;
|
|
30
|
+
planId: string;
|
|
31
|
+
billingCycle: BillingCycle;
|
|
32
|
+
/** For the firstTimeCustomersOnly check. */
|
|
33
|
+
email?: string;
|
|
34
|
+
ipHash?: string;
|
|
35
|
+
sessionId?: string;
|
|
36
|
+
}
|
|
37
|
+
interface RedeemInput {
|
|
38
|
+
code: string;
|
|
39
|
+
subscriptionId: string;
|
|
40
|
+
tenantId: string;
|
|
41
|
+
/** For the firstTimeCustomersOnly check in the final redeem path. */
|
|
42
|
+
email?: string;
|
|
43
|
+
}
|
|
44
|
+
interface PromoServiceConfig {
|
|
45
|
+
/** Plans that are not discountable (e.g. 'ENTERPRISE'). */
|
|
46
|
+
nonRedeemablePlans?: readonly string[];
|
|
47
|
+
}
|
|
48
|
+
interface PromoCodeStats {
|
|
49
|
+
code: PromoCodeRecord;
|
|
50
|
+
validations: number;
|
|
51
|
+
redemptions: {
|
|
52
|
+
active: number;
|
|
53
|
+
total: number;
|
|
54
|
+
reversed: number;
|
|
55
|
+
expired: number;
|
|
56
|
+
};
|
|
57
|
+
/** Decimal-as-string ('0.00' if no aggregator is available). */
|
|
58
|
+
revenueDeductionGross: string;
|
|
59
|
+
}
|
|
60
|
+
declare class PromoCodesService {
|
|
61
|
+
private readonly promoRepo;
|
|
62
|
+
private readonly redemptionRepo;
|
|
63
|
+
private readonly validationLogRepo;
|
|
64
|
+
private readonly firstTimeCheck;
|
|
65
|
+
private readonly subscriptionLookup;
|
|
66
|
+
private readonly revenueAggregator;
|
|
67
|
+
private readonly transactionRunner;
|
|
68
|
+
private readonly planCatalog;
|
|
69
|
+
private readonly config;
|
|
70
|
+
constructor(promoRepo: PromoCodeRepository, redemptionRepo: PromoCodeRedemptionRepository, validationLogRepo: PromoCodeValidationLogRepository, firstTimeCheck: FirstTimeCustomerCheck, subscriptionLookup: PromoSubscriptionLookup, revenueAggregator: PromoRevenueDeductionAggregator, transactionRunner: TransactionRunner, planCatalog: PlanCatalog, config: PromoServiceConfig);
|
|
71
|
+
create(input: CreatePromoCodeData): Promise<PromoCodeRecord>;
|
|
72
|
+
update(id: string, input: UpdatePromoCodeData): Promise<PromoCodeRecord>;
|
|
73
|
+
softDelete(id: string): Promise<void>;
|
|
74
|
+
findAll(filter?: PromoCodeFilter): Promise<PromoCodeRecord[]>;
|
|
75
|
+
findOne(id: string): Promise<PromoCodeRecord>;
|
|
76
|
+
stats(id: string): Promise<PromoCodeStats>;
|
|
77
|
+
listRedemptions(promoCodeId: string): Promise<PromoCodeRedemptionListItem[]>;
|
|
78
|
+
preview(input: PreviewInput): Promise<PreviewResult>;
|
|
79
|
+
/**
|
|
80
|
+
* Wraps `redeemInTransaction` in its own transaction-runner call —
|
|
81
|
+
* the default path when the caller has no external tx context
|
|
82
|
+
* (e.g. POST /billing/promo/redeem as a stand-alone endpoint).
|
|
83
|
+
*/
|
|
84
|
+
redeem(input: RedeemInput): Promise<PromoCodeRedemptionRecord>;
|
|
85
|
+
/**
|
|
86
|
+
* Redeems a promo code within an EXTERNAL transaction — the caller
|
|
87
|
+
* (typically `TenantSubscriptionWritePort.applyOnboardingSelection`)
|
|
88
|
+
* already has `prisma.$transaction(...)` open and passes the `tx`
|
|
89
|
+
* context through. This lands plan change, add-on insert, and redemption
|
|
90
|
+
* insert in a single DB transaction (P10.1.1).
|
|
91
|
+
*/
|
|
92
|
+
redeemInTransaction(input: RedeemInput, tx: TransactionContext): Promise<PromoCodeRedemptionRecord>;
|
|
93
|
+
reverse(subscriptionId: string): Promise<PromoCodeRedemptionRecord | null>;
|
|
94
|
+
private checkEligibility;
|
|
95
|
+
private checkFirstTimeCustomer;
|
|
96
|
+
/**
|
|
97
|
+
* Lazy expiry: sets codes whose validUntil has passed to EXPIRED.
|
|
98
|
+
* Called before every find/preview — defense-in-depth alongside the cron.
|
|
99
|
+
*/
|
|
100
|
+
private lazyExpire;
|
|
101
|
+
/**
|
|
102
|
+
* Lowest applicable plan price. With a whitelist it takes the minimum
|
|
103
|
+
* from the whitelist, otherwise across all marketed plans of the catalog
|
|
104
|
+
* (except non-redeemable).
|
|
105
|
+
*/
|
|
106
|
+
private lowestApplicablePlanGross;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export { type PreviewInput as P, type RedeemInput as R, type PreviewInvalid as a, type PreviewReason as b, type PreviewResult as c, type PreviewValid as d, type PromoCodeStats as e, PromoCodesService as f, type PromoServiceConfig as g };
|