@saasicat/adapter-prisma 0.3.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.
@@ -0,0 +1,859 @@
1
+ import { PersistenceInjectionToken, PasswordHasher, SaasicatPersistenceAdapter, TransactionRunner, TransactionContext, MfaPort, AuditPort, AdminActor, AuditQueryPort, AuditQuery, AuditEntry, AuditStatsPort, RlsBypassPort, SubscriptionRepository, SubscriptionRecord, PlanVersionRepository, PlanVersionRecord, PromoCodeRepository, PromoCodeRecord, PromoCodeFilter, CreatePromoCodeData, UpdatePromoCodeData, PromoCodeRedemptionRepository, PromoCodeRedemptionRecord, PromoCodeRedemptionStatus, PromoCodeRedemptionListItem, PromoCodeValidationLogRepository, PromoSubscriptionLookup, BillingCycle, PromoRevenueDeductionAggregator, SuperAdminProvisioningPort, CreateSuperAdminCliInput, PlatformUserDto, PlanCatalogReadSink, PlanCatalogReadSnapshot, PlanCatalogImportSink, UpsertPlanInput, UpsertResult, UpsertPlanVersionInput, UpsertFeatureCatalogEntryInput } from '@saasicat/types';
2
+
3
+ declare const PRISMA_CLIENT_TOKEN: unique symbol;
4
+ /** Prisma `Decimal` values arrive as objects with `toString()`; tests may use plain values. */
5
+ type DecimalLike = {
6
+ toString(): string;
7
+ } | string | number;
8
+ interface SubscriptionRowLike {
9
+ id: string;
10
+ tenantId: string;
11
+ plan: string;
12
+ billingCycle: string;
13
+ status: string;
14
+ isPilot: boolean;
15
+ trialEntitlementPlan: string | null;
16
+ pendingPlan: string | null;
17
+ pendingEffectiveAt: Date | null;
18
+ customLimits: unknown;
19
+ planVersionId: string | null;
20
+ pendingPlanVersionId: string | null;
21
+ startedAt: Date | null;
22
+ }
23
+ interface PlanVersionRowLike {
24
+ id: string;
25
+ planId: string;
26
+ version: number;
27
+ baseVersionId: string | null;
28
+ features: unknown;
29
+ quotas: unknown;
30
+ monthlyNet: DecimalLike;
31
+ yearlyNet: DecimalLike;
32
+ marketed: boolean;
33
+ publishedAt: Date | null;
34
+ supersededAt: Date | null;
35
+ publishedChanges: unknown;
36
+ changeNote: string;
37
+ nonRegressive: boolean;
38
+ createdByUserId: string | null;
39
+ publishedByUserId: string | null;
40
+ createdAt: Date;
41
+ updatedAt: Date;
42
+ }
43
+ interface PlanRowLike {
44
+ id: string;
45
+ projectKey: string;
46
+ planKey: string;
47
+ label: string;
48
+ description: string | null;
49
+ icon: string | null;
50
+ sortOrder: number;
51
+ createdAt: Date;
52
+ updatedAt: Date;
53
+ deletedAt: Date | null;
54
+ }
55
+ interface FeatureCatalogEntryRowLike {
56
+ id: string;
57
+ projectKey: string;
58
+ featureKey: string;
59
+ label: string;
60
+ description: string | null;
61
+ marketingLabel: string | null;
62
+ marketingDescription: string | null;
63
+ icon: string | null;
64
+ tier: string | null;
65
+ discoveryStatus: string;
66
+ requires: string[];
67
+ replaces: string[];
68
+ successorKey: string | null;
69
+ approvedAt: Date | null;
70
+ approvedBy: string | null;
71
+ approvedSignature: string | null;
72
+ plannedOnly: boolean;
73
+ core: boolean;
74
+ i18n: unknown;
75
+ sortOrder: number;
76
+ createdAt: Date;
77
+ updatedAt: Date;
78
+ deletedAt: Date | null;
79
+ }
80
+ interface PromoCodeRowLike {
81
+ id: string;
82
+ code: string;
83
+ valueType: string;
84
+ value: DecimalLike;
85
+ durationType: string;
86
+ durationValue: number | null;
87
+ validFrom: Date | null;
88
+ validUntil: Date | null;
89
+ maxRedemptions: number | null;
90
+ redemptionsCount: number;
91
+ appliesToPlans: string[];
92
+ appliesToBilling: string | null;
93
+ firstTimeCustomersOnly: boolean;
94
+ minimumPlanAmountGross: DecimalLike | null;
95
+ allowZeroInvoice: boolean;
96
+ status: string;
97
+ description: string | null;
98
+ campaignTag: string | null;
99
+ revenueDeductionAccount: string | null;
100
+ createdById: string;
101
+ createdAt: Date;
102
+ updatedAt: Date;
103
+ deletedAt: Date | null;
104
+ }
105
+ interface PromoCodeRedemptionRowLike {
106
+ id: string;
107
+ promoCodeId: string;
108
+ subscriptionId: string;
109
+ tenantId: string;
110
+ appliedValueType: string;
111
+ appliedValue: DecimalLike;
112
+ appliedDurationType: string;
113
+ appliedDurationValue: number | null;
114
+ startsAt: Date;
115
+ endsAt: Date | null;
116
+ status: string;
117
+ redeemedAt: Date;
118
+ reversedAt: Date | null;
119
+ }
120
+ interface AuditLogRowLike {
121
+ id: string;
122
+ tenantId: string | null;
123
+ userId: string | null;
124
+ entity: string;
125
+ entityId: string;
126
+ action: string;
127
+ changes: unknown;
128
+ actorTag: string | null;
129
+ ipAddress: string | null;
130
+ userAgent: string | null;
131
+ createdAt: Date;
132
+ }
133
+ interface SuperAdminUserRowLike {
134
+ id: string;
135
+ email: string;
136
+ passwordHash: string;
137
+ firstName: string | null;
138
+ lastName: string | null;
139
+ platformRole: string;
140
+ isActive: boolean;
141
+ lastLoginAt: Date | null;
142
+ deletedAt: Date | null;
143
+ createdAt: Date;
144
+ updatedAt: Date;
145
+ }
146
+ type SortDirection = 'asc' | 'desc';
147
+ interface SubscriptionDelegateLike {
148
+ findUnique(args: {
149
+ where: {
150
+ id?: string;
151
+ tenantId?: string;
152
+ };
153
+ }): Promise<SubscriptionRowLike | null>;
154
+ findMany(args?: {
155
+ where?: {
156
+ status?: {
157
+ in: string[];
158
+ };
159
+ };
160
+ }): Promise<SubscriptionRowLike[]>;
161
+ count(args?: {
162
+ where?: {
163
+ OR?: Array<{
164
+ planVersionId?: string;
165
+ pendingPlanVersionId?: string;
166
+ }>;
167
+ };
168
+ }): Promise<number>;
169
+ }
170
+ interface PlanVersionDelegateLike {
171
+ findUnique(args: {
172
+ where: {
173
+ id: string;
174
+ };
175
+ }): Promise<PlanVersionRowLike | null>;
176
+ findFirst(args: {
177
+ where: {
178
+ planId?: string;
179
+ version?: number;
180
+ publishedAt?: {
181
+ not: null;
182
+ } | null;
183
+ supersededAt?: null;
184
+ };
185
+ orderBy?: {
186
+ version?: SortDirection;
187
+ };
188
+ }): Promise<PlanVersionRowLike | null>;
189
+ findMany(args?: {
190
+ where?: {
191
+ planId?: {
192
+ in: string[];
193
+ };
194
+ publishedAt?: {
195
+ not: null;
196
+ } | null;
197
+ supersededAt?: null;
198
+ };
199
+ }): Promise<PlanVersionRowLike[]>;
200
+ create(args: {
201
+ data: {
202
+ planId: string;
203
+ version: number;
204
+ features: unknown;
205
+ quotas: unknown;
206
+ monthlyNet: string;
207
+ yearlyNet: string;
208
+ marketed: boolean;
209
+ publishedAt: Date | null;
210
+ changeNote: string;
211
+ };
212
+ }): Promise<PlanVersionRowLike>;
213
+ updateMany(args: {
214
+ where: {
215
+ planId?: string;
216
+ publishedAt?: {
217
+ not: null;
218
+ } | null;
219
+ supersededAt?: null;
220
+ version?: {
221
+ lt: number;
222
+ };
223
+ };
224
+ data: {
225
+ supersededAt?: Date;
226
+ };
227
+ }): Promise<{
228
+ count: number;
229
+ }>;
230
+ }
231
+ interface PlanDelegateLike {
232
+ findFirst(args: {
233
+ where: {
234
+ projectKey?: string;
235
+ planKey?: string;
236
+ deletedAt?: null;
237
+ };
238
+ }): Promise<PlanRowLike | null>;
239
+ findMany(args?: {
240
+ where?: {
241
+ projectKey?: string;
242
+ deletedAt?: null;
243
+ };
244
+ orderBy?: {
245
+ sortOrder?: SortDirection;
246
+ };
247
+ }): Promise<PlanRowLike[]>;
248
+ create(args: {
249
+ data: {
250
+ projectKey: string;
251
+ planKey: string;
252
+ label: string;
253
+ description?: string | null;
254
+ sortOrder?: number;
255
+ };
256
+ }): Promise<PlanRowLike>;
257
+ }
258
+ interface FeatureCatalogEntryDelegateLike {
259
+ findFirst(args: {
260
+ where: {
261
+ projectKey?: string;
262
+ featureKey?: string;
263
+ deletedAt?: null;
264
+ };
265
+ }): Promise<FeatureCatalogEntryRowLike | null>;
266
+ findMany(args?: {
267
+ where?: {
268
+ projectKey?: string;
269
+ deletedAt?: null;
270
+ };
271
+ orderBy?: {
272
+ sortOrder?: SortDirection;
273
+ };
274
+ }): Promise<FeatureCatalogEntryRowLike[]>;
275
+ create(args: {
276
+ data: {
277
+ projectKey: string;
278
+ featureKey: string;
279
+ label: string;
280
+ icon?: string | null;
281
+ tier?: string | null;
282
+ plannedOnly?: boolean;
283
+ core?: boolean;
284
+ };
285
+ }): Promise<FeatureCatalogEntryRowLike>;
286
+ }
287
+ interface PromoCodeDelegateLike {
288
+ findUnique(args: {
289
+ where: {
290
+ id?: string;
291
+ code?: string;
292
+ };
293
+ }): Promise<PromoCodeRowLike | null>;
294
+ findMany(args?: {
295
+ where?: {
296
+ deletedAt?: null;
297
+ status?: string;
298
+ campaignTag?: string;
299
+ code?: {
300
+ contains: string;
301
+ };
302
+ };
303
+ orderBy?: {
304
+ createdAt?: SortDirection;
305
+ };
306
+ }): Promise<PromoCodeRowLike[]>;
307
+ create(args: {
308
+ data: {
309
+ code: string;
310
+ valueType: string;
311
+ value: string;
312
+ durationType: string;
313
+ durationValue: number | null;
314
+ validFrom: Date | null;
315
+ validUntil: Date | null;
316
+ maxRedemptions: number | null;
317
+ appliesToPlans: string[];
318
+ appliesToBilling: string | null;
319
+ firstTimeCustomersOnly: boolean;
320
+ minimumPlanAmountGross: string | null;
321
+ allowZeroInvoice: boolean;
322
+ description: string | null;
323
+ campaignTag: string | null;
324
+ revenueDeductionAccount: string | null;
325
+ createdById: string;
326
+ };
327
+ }): Promise<PromoCodeRowLike>;
328
+ update(args: {
329
+ where: {
330
+ id: string;
331
+ };
332
+ data: {
333
+ status?: string;
334
+ description?: string | null;
335
+ validUntil?: Date | null;
336
+ maxRedemptions?: number | null;
337
+ deletedAt?: Date;
338
+ };
339
+ }): Promise<PromoCodeRowLike>;
340
+ updateMany(args: {
341
+ where: {
342
+ status?: {
343
+ in: string[];
344
+ };
345
+ validUntil?: {
346
+ lt: Date;
347
+ };
348
+ };
349
+ data: {
350
+ status?: string;
351
+ };
352
+ }): Promise<{
353
+ count: number;
354
+ }>;
355
+ }
356
+ interface PromoCodeRedemptionDelegateLike {
357
+ findUnique(args: {
358
+ where: {
359
+ id?: string;
360
+ subscriptionId?: string;
361
+ };
362
+ }): Promise<PromoCodeRedemptionRowLike | null>;
363
+ findMany(args?: {
364
+ where?: {
365
+ promoCodeId?: string;
366
+ };
367
+ orderBy?: {
368
+ redeemedAt?: SortDirection;
369
+ };
370
+ }): Promise<PromoCodeRedemptionRowLike[]>;
371
+ create(args: {
372
+ data: {
373
+ promoCodeId: string;
374
+ subscriptionId: string;
375
+ tenantId: string;
376
+ appliedValueType: string;
377
+ appliedValue: string;
378
+ appliedDurationType: string;
379
+ appliedDurationValue: number | null;
380
+ startsAt: Date;
381
+ endsAt: Date | null;
382
+ };
383
+ }): Promise<PromoCodeRedemptionRowLike>;
384
+ update(args: {
385
+ where: {
386
+ id: string;
387
+ };
388
+ data: {
389
+ status?: string;
390
+ reversedAt?: Date | null;
391
+ };
392
+ }): Promise<PromoCodeRedemptionRowLike>;
393
+ count(args?: {
394
+ where?: {
395
+ promoCodeId?: string;
396
+ status?: string;
397
+ };
398
+ }): Promise<number>;
399
+ updateMany(args: {
400
+ where: {
401
+ status?: string;
402
+ endsAt?: {
403
+ lt: Date;
404
+ };
405
+ };
406
+ data: {
407
+ status?: string;
408
+ };
409
+ }): Promise<{
410
+ count: number;
411
+ }>;
412
+ }
413
+ interface PromoCodeValidationLogDelegateLike {
414
+ create(args: {
415
+ data: {
416
+ promoCodeId: string | null;
417
+ codeAttempt: string;
418
+ result: string;
419
+ ipHash?: string | null;
420
+ sessionId?: string | null;
421
+ };
422
+ }): Promise<unknown>;
423
+ count(args?: {
424
+ where?: {
425
+ promoCodeId?: string;
426
+ result?: string;
427
+ };
428
+ }): Promise<number>;
429
+ }
430
+ interface AuditLogDelegateLike {
431
+ create(args: {
432
+ data: {
433
+ tenantId: string | null;
434
+ userId: string | null;
435
+ entity: string;
436
+ entityId: string;
437
+ action: string;
438
+ changes: unknown;
439
+ actorTag: string | null;
440
+ };
441
+ }): Promise<unknown>;
442
+ findMany(args?: {
443
+ where?: {
444
+ tenantId?: string;
445
+ userId?: string;
446
+ entity?: string;
447
+ entityId?: string;
448
+ action?: string;
449
+ actorTag?: string | {
450
+ startsWith: string;
451
+ };
452
+ createdAt?: {
453
+ gte?: Date;
454
+ lte?: Date;
455
+ };
456
+ };
457
+ orderBy?: {
458
+ createdAt?: SortDirection;
459
+ };
460
+ skip?: number;
461
+ take?: number;
462
+ }): Promise<AuditLogRowLike[]>;
463
+ count(args?: {
464
+ where?: {
465
+ createdAt?: {
466
+ gte?: Date;
467
+ };
468
+ };
469
+ }): Promise<number>;
470
+ }
471
+ interface SuperAdminUserDelegateLike {
472
+ findUnique(args: {
473
+ where: {
474
+ email: string;
475
+ };
476
+ }): Promise<SuperAdminUserRowLike | null>;
477
+ count(args?: {
478
+ where?: {
479
+ isActive?: boolean;
480
+ deletedAt?: null;
481
+ };
482
+ }): Promise<number>;
483
+ create(args: {
484
+ data: {
485
+ email: string;
486
+ passwordHash: string;
487
+ firstName?: string | null;
488
+ lastName?: string | null;
489
+ };
490
+ }): Promise<SuperAdminUserRowLike>;
491
+ }
492
+ interface SuperAdminMfaDelegateLike {
493
+ findUnique(args: {
494
+ where: {
495
+ userId: string;
496
+ };
497
+ }): Promise<{
498
+ userId: string;
499
+ secret: string | null;
500
+ enabledAt: Date | null;
501
+ updatedAt: Date;
502
+ } | null>;
503
+ upsert(args: {
504
+ where: {
505
+ userId: string;
506
+ };
507
+ create: {
508
+ userId: string;
509
+ secret: string | null;
510
+ enabledAt: Date | null;
511
+ };
512
+ update: {
513
+ secret: string | null;
514
+ enabledAt: Date | null;
515
+ };
516
+ }): Promise<unknown>;
517
+ }
518
+ /**
519
+ * Structural sub-interface of a Prisma transaction client
520
+ * (`Prisma.TransactionClient`): the table delegates plus raw access, without
521
+ * `$transaction`. Repository methods that accept an opaque
522
+ * `TransactionContext` cast it to this shape.
523
+ */
524
+ interface PrismaTxLike {
525
+ subscription: SubscriptionDelegateLike;
526
+ planVersion: PlanVersionDelegateLike;
527
+ plan: PlanDelegateLike;
528
+ featureCatalogEntry: FeatureCatalogEntryDelegateLike;
529
+ promoCode: PromoCodeDelegateLike;
530
+ promoCodeRedemption: PromoCodeRedemptionDelegateLike;
531
+ promoCodeValidationLog: PromoCodeValidationLogDelegateLike;
532
+ auditLog: AuditLogDelegateLike;
533
+ superAdminUser: SuperAdminUserDelegateLike;
534
+ superAdminMfa: SuperAdminMfaDelegateLike;
535
+ $queryRaw(query: TemplateStringsArray, ...values: unknown[]): Promise<unknown>;
536
+ $executeRaw(query: TemplateStringsArray, ...values: unknown[]): Promise<number>;
537
+ }
538
+ /**
539
+ * Structural sub-interface of `@prisma/client.PrismaClient`. The adapters
540
+ * expect only the delegates they actually use — no hard import on
541
+ * `@prisma/client`, so the package builds without a Prisma generate and can be
542
+ * mocked in tests.
543
+ *
544
+ * A consumer's `PrismaService extends PrismaClient` (generated from the
545
+ * canonical prisma-fragments schema) satisfies the interface automatically.
546
+ */
547
+ interface PrismaLike extends PrismaTxLike {
548
+ $transaction<T>(fn: (tx: PrismaTxLike) => Promise<T>): Promise<T>;
549
+ }
550
+
551
+ interface PrismaPersistenceOptions {
552
+ /**
553
+ * The app's Prisma client: either its injection token (typically the
554
+ * `PrismaService` class) — resolved through Nest DI at boot — or a ready
555
+ * `PrismaLike` instance (tests, non-DI scripts).
556
+ */
557
+ client: PrismaLike | PersistenceInjectionToken;
558
+ /**
559
+ * App `PasswordHasher` (token or instance). Enables
560
+ * `core.superAdminProvisioning` (setup wizard / create-super-admin);
561
+ * without it the slice member stays absent.
562
+ */
563
+ passwordHasher?: PasswordHasher | PersistenceInjectionToken;
564
+ /**
565
+ * Set to true when the app's Prisma middleware really lifts RLS
566
+ * (`SET LOCAL row_security = off`) while
567
+ * `AsyncLocalRlsBypassAdapter.isBypassActive()`. Only toggles the
568
+ * declared `rowLevelSecurity` capability — the adapter cannot verify the
569
+ * middleware. Default false.
570
+ */
571
+ rlsIntegration?: boolean;
572
+ }
573
+ /**
574
+ * Builds the `SaasicatPersistenceAdapter` bundle for Prisma + PostgreSQL on
575
+ * the canonical schema (`@saasicat/spec` prisma-fragments):
576
+ *
577
+ * ```ts
578
+ * SaasPlatformModule.forRoot({
579
+ * persistence: prismaPersistence({ client: PrismaService }),
580
+ * // ...
581
+ * });
582
+ * ```
583
+ *
584
+ * Slices not shipped by this adapter (contracts, bundles, registration,
585
+ * tenant-billing write ports) stay absent — consumers keep providing custom
586
+ * adapters for those features.
587
+ */
588
+ declare function prismaPersistence(options: PrismaPersistenceOptions): SaasicatPersistenceAdapter;
589
+
590
+ /**
591
+ * `TransactionRunner` over `prisma.$transaction`. The interactive transaction
592
+ * client is passed through as the opaque `TransactionContext`; every
593
+ * repository in this package resolves it back via `resolveClient`.
594
+ */
595
+ declare class PrismaTransactionRunner implements TransactionRunner {
596
+ private readonly prisma;
597
+ constructor(prisma: PrismaLike);
598
+ run<T>(fn: (tx: TransactionContext) => Promise<T>): Promise<T>;
599
+ }
600
+
601
+ /**
602
+ * Default implementation of `MfaPort` against the `SuperAdminMfa` table
603
+ * from the platform Prisma fragment.
604
+ *
605
+ * Schema assumption:
606
+ *
607
+ * ```prisma
608
+ * model SuperAdminMfa {
609
+ * userId String @id
610
+ * secret String?
611
+ * enabledAt DateTime?
612
+ * updatedAt DateTime @updatedAt
613
+ * }
614
+ * ```
615
+ */
616
+ declare class PrismaMfaAdapter implements MfaPort {
617
+ private readonly prisma;
618
+ constructor(prisma: PrismaLike);
619
+ getSecret(userId: string): Promise<string | null>;
620
+ setSecret(userId: string, secret: string | null): Promise<void>;
621
+ isEnabled(userId: string): Promise<boolean>;
622
+ }
623
+
624
+ /** `'web:<email>:<sessionId>'` / `'cli:<email>:<host>'` — audit-event.schema.json ActorTagPattern. */
625
+ declare function buildActorTag(actor: AdminActor): string;
626
+ /**
627
+ * Default implementation of `AuditPort` against the canonical `audit_logs`
628
+ * table (`@saasicat/spec` prisma-fragments/04-audit-log.prisma).
629
+ *
630
+ * SuperAdmin actions are platform-global, so `tenantId` stays null; the
631
+ * actor lands as `userId` + `actorTag`.
632
+ */
633
+ declare class PrismaAuditAdapter implements AuditPort {
634
+ private readonly prisma;
635
+ constructor(prisma: PrismaLike);
636
+ write(input: {
637
+ actor: AdminActor;
638
+ entity: string;
639
+ entityId: string;
640
+ action: string;
641
+ changes?: Record<string, unknown>;
642
+ }): Promise<void>;
643
+ }
644
+
645
+ /**
646
+ * `AuditQueryPort` against the canonical `audit_logs` table. Powers
647
+ * `<app> audit tail` and the admin audit pages.
648
+ */
649
+ declare class PrismaAuditQueryAdapter implements AuditQueryPort {
650
+ private readonly prisma;
651
+ constructor(prisma: PrismaLike);
652
+ list(filter: AuditQuery): Promise<AuditEntry[]>;
653
+ }
654
+
655
+ /** `AuditStatsPort` against the canonical `audit_logs` table. */
656
+ declare class PrismaAuditStatsAdapter implements AuditStatsPort {
657
+ private readonly prisma;
658
+ constructor(prisma: PrismaLike);
659
+ countSince(since: Date): Promise<number>;
660
+ }
661
+
662
+ /**
663
+ * Default implementation of `RlsBypassPort` via `node:async_hooks`.
664
+ *
665
+ * Sets `bypass: true` for the duration of the given callback in the current
666
+ * async context. Your PrismaService reads `isBypassActive()` (e.g. in a
667
+ * Prisma middleware or an interceptor) and issues `SET LOCAL row_security
668
+ * = off` in the current transaction when active.
669
+ *
670
+ * Example middleware:
671
+ *
672
+ * ```ts
673
+ * constructor(private readonly rls: AsyncLocalRlsBypassAdapter) {
674
+ * super();
675
+ * this.$use(async (params, next) => {
676
+ * if (this.rls.isBypassActive() && params.action.startsWith('find')) {
677
+ * // Bypass mode active — disable RLS for this query.
678
+ * await this.$executeRawUnsafe('SET LOCAL row_security = off');
679
+ * }
680
+ * return next(params);
681
+ * });
682
+ * }
683
+ * ```
684
+ */
685
+ declare class AsyncLocalRlsBypassAdapter implements RlsBypassPort {
686
+ private readonly storage;
687
+ runWithBypass<T>(fn: () => Promise<T>): Promise<T>;
688
+ /**
689
+ * `true` during the execution of a `runWithBypass(...)` callback.
690
+ * Query this in the PrismaService or an interceptor.
691
+ */
692
+ isBypassActive(): boolean;
693
+ }
694
+
695
+ /**
696
+ * `SubscriptionRepository` against the canonical `subscriptions` +
697
+ * `plan_versions` tables.
698
+ *
699
+ * Limitation: subscriptions that bind ONLY a `businessTypeVersionId` (no
700
+ * `planVersionId`) are not supported by this shipped adapter — the
701
+ * BusinessType composition needs app-specific aggregation. Such rows raise a
702
+ * descriptive error instead of returning wrong entitlements.
703
+ *
704
+ * `countByBundleVersionId` is deliberately not implemented (the
705
+ * `subscription_bundles` junction is not part of this adapter's slice);
706
+ * the platform then treats affected bundle versions as frozen (fail-closed).
707
+ */
708
+ declare class PrismaSubscriptionRepository implements SubscriptionRepository {
709
+ private readonly prisma;
710
+ constructor(prisma: PrismaLike);
711
+ findByTenantId(tenantId: string): Promise<SubscriptionRecord | null>;
712
+ findByTenantIdLocked(tenantId: string, tx: TransactionContext): Promise<SubscriptionRecord | null>;
713
+ countByPlanVersionId(planVersionId: string): Promise<number>;
714
+ countActiveByPlanKey(_projectKey: string): Promise<Record<string, number>>;
715
+ private loadByTenantId;
716
+ private toRecord;
717
+ }
718
+
719
+ /**
720
+ * `PlanVersionRepository` against the canonical `plan_versions` table.
721
+ *
722
+ * `findActive` is deliberately not implemented: the canonical schema carries
723
+ * no `validFrom`/`validUntil` columns yet, so time-aware resolution is not
724
+ * expressible — consumers fall back to `findLatestLive` (documented port
725
+ * behavior).
726
+ */
727
+ declare class PrismaPlanVersionRepository implements PlanVersionRepository {
728
+ private readonly prisma;
729
+ constructor(prisma: PrismaLike);
730
+ findLatestLive(planId: string, tx?: TransactionContext): Promise<PlanVersionRecord | null>;
731
+ }
732
+
733
+ /**
734
+ * `PromoCodeRepository` against the canonical `promo_codes` table.
735
+ *
736
+ * The availability-critical mutations (`claimSlot`, `releaseSlot`,
737
+ * `markExhaustedIfFull`) run as single atomic UPDATE statements — the
738
+ * column-to-column guard (`redemptionsCount < maxRedemptions`) is not
739
+ * expressible in the Prisma query API, so they use `$executeRaw`. The raw
740
+ * statements maintain `updatedAt` manually because they bypass Prisma's
741
+ * `@updatedAt`.
742
+ */
743
+ declare class PrismaPromoCodeRepository implements PromoCodeRepository {
744
+ private readonly prisma;
745
+ constructor(prisma: PrismaLike);
746
+ findById(id: string): Promise<PromoCodeRecord | null>;
747
+ findByCode(code: string, tx?: TransactionContext): Promise<PromoCodeRecord | null>;
748
+ findMany(filter: PromoCodeFilter): Promise<PromoCodeRecord[]>;
749
+ create(data: CreatePromoCodeData): Promise<PromoCodeRecord>;
750
+ update(id: string, data: UpdatePromoCodeData): Promise<PromoCodeRecord>;
751
+ softDelete(id: string): Promise<void>;
752
+ claimSlot(id: string, tx?: TransactionContext): Promise<boolean>;
753
+ markExhaustedIfFull(id: string, tx?: TransactionContext): Promise<void>;
754
+ releaseSlot(id: string, tx?: TransactionContext): Promise<void>;
755
+ expireDueCodes(now: Date): Promise<number>;
756
+ }
757
+
758
+ /**
759
+ * `PromoCodeRedemptionRepository` against the canonical
760
+ * `promo_code_redemptions` table. Double redemption per subscription is
761
+ * excluded by the `subscriptionId` unique constraint — a concurrent second
762
+ * `create` fails at the database.
763
+ */
764
+ declare class PrismaPromoCodeRedemptionRepository implements PromoCodeRedemptionRepository {
765
+ private readonly prisma;
766
+ constructor(prisma: PrismaLike);
767
+ findBySubscription(subscriptionId: string, tx?: TransactionContext): Promise<PromoCodeRedemptionRecord | null>;
768
+ create(data: Omit<PromoCodeRedemptionRecord, 'id' | 'redeemedAt' | 'status' | 'reversedAt'>, tx?: TransactionContext): Promise<PromoCodeRedemptionRecord>;
769
+ setReversed(id: string, tx?: TransactionContext): Promise<PromoCodeRedemptionRecord>;
770
+ countByPromoCode(promoCodeId: string, status?: PromoCodeRedemptionStatus): Promise<number>;
771
+ listByPromoCode(promoCodeId: string): Promise<PromoCodeRedemptionListItem[]>;
772
+ expireDueRedemptions(now: Date): Promise<number>;
773
+ }
774
+
775
+ /** `PromoCodeValidationLogRepository` against `promo_code_validation_logs`. */
776
+ declare class PrismaPromoCodeValidationLogRepository implements PromoCodeValidationLogRepository {
777
+ private readonly prisma;
778
+ constructor(prisma: PrismaLike);
779
+ log(args: {
780
+ promoCodeId: string | null;
781
+ codeAttempt: string;
782
+ result: string;
783
+ ipHash?: string;
784
+ sessionId?: string;
785
+ }): Promise<void>;
786
+ countValid(promoCodeId: string): Promise<number>;
787
+ }
788
+
789
+ /** `PromoSubscriptionLookup` against the canonical `subscriptions` table. */
790
+ declare class PrismaPromoSubscriptionLookup implements PromoSubscriptionLookup {
791
+ private readonly prisma;
792
+ constructor(prisma: PrismaLike);
793
+ findById(subscriptionId: string, tx?: TransactionContext): Promise<{
794
+ id: string;
795
+ tenantId: string;
796
+ plan: string;
797
+ billingCycle: BillingCycle;
798
+ startedAt: Date | null;
799
+ } | null>;
800
+ }
801
+
802
+ /**
803
+ * Default `PromoRevenueDeductionAggregator` for apps without an
804
+ * `InvoiceDiscount` table: always reports `'0.00'` (the documented port
805
+ * fallback). Apps with invoice discounts replace it with their own adapter.
806
+ */
807
+ declare class ZeroPromoRevenueDeductionAggregator implements PromoRevenueDeductionAggregator {
808
+ sumGrossForPromoCode(_promoCodeId: string): Promise<string>;
809
+ }
810
+
811
+ /**
812
+ * DI token for the app's `PasswordHasher` implementation (argon2/bcrypt —
813
+ * the algorithm stays app-specific). `prismaPersistence({ passwordHasher })`
814
+ * wires it automatically; manual setups bind it themselves.
815
+ */
816
+ declare const PASSWORD_HASHER_TOKEN: unique symbol;
817
+ /**
818
+ * `SuperAdminProvisioningPort` against the canonical `super_admin_users`
819
+ * table (prisma-fragments/10-super-admin.prisma). Backs the first-run setup
820
+ * wizard and `<app> user create-super-admin`.
821
+ */
822
+ declare class PrismaSuperAdminBootstrapAdapter implements SuperAdminProvisioningPort {
823
+ private readonly prisma;
824
+ private readonly passwordHasher;
825
+ constructor(prisma: PrismaLike, passwordHasher: PasswordHasher);
826
+ countSuperAdmins(): Promise<number>;
827
+ createSuperAdmin(input: CreateSuperAdminCliInput): Promise<PlatformUserDto>;
828
+ }
829
+
830
+ /**
831
+ * `PlanCatalogReadSink` against the canonical `plans`, `plan_versions` and
832
+ * `feature_catalog_entries` tables — DB hydration of the plan catalog at
833
+ * boot (`PlanCatalogModule.forRoot({ sink })`).
834
+ *
835
+ * `validFrom`/`validUntil` are reported as null: the canonical schema does
836
+ * not persist booking windows yet (see docs/data-model.md, "Known gaps").
837
+ */
838
+ declare class PrismaPlanCatalogReadSink implements PlanCatalogReadSink {
839
+ private readonly prisma;
840
+ constructor(prisma: PrismaLike);
841
+ loadSnapshot(projectKey: string): Promise<PlanCatalogReadSnapshot>;
842
+ }
843
+
844
+ /**
845
+ * `PlanCatalogImportSink` against the canonical catalog tables — the
846
+ * one-shot `saas.yaml → DB` import at boot.
847
+ *
848
+ * Idempotency per the port contract: existing rows (identity match) are
849
+ * skipped without error; only the created/skipped flags are reported.
850
+ */
851
+ declare class PrismaPlanCatalogImportSink implements PlanCatalogImportSink {
852
+ private readonly prisma;
853
+ constructor(prisma: PrismaLike);
854
+ upsertPlan(input: UpsertPlanInput): Promise<UpsertResult>;
855
+ upsertPlanVersion(input: UpsertPlanVersionInput): Promise<UpsertResult>;
856
+ upsertFeatureCatalogEntry(input: UpsertFeatureCatalogEntryInput): Promise<UpsertResult>;
857
+ }
858
+
859
+ export { AsyncLocalRlsBypassAdapter, type DecimalLike, PASSWORD_HASHER_TOKEN, PRISMA_CLIENT_TOKEN, PrismaAuditAdapter, PrismaAuditQueryAdapter, PrismaAuditStatsAdapter, type PrismaLike, PrismaMfaAdapter, type PrismaPersistenceOptions, PrismaPlanCatalogImportSink, PrismaPlanCatalogReadSink, PrismaPlanVersionRepository, PrismaPromoCodeRedemptionRepository, PrismaPromoCodeRepository, PrismaPromoCodeValidationLogRepository, PrismaPromoSubscriptionLookup, PrismaSubscriptionRepository, PrismaSuperAdminBootstrapAdapter, PrismaTransactionRunner, type PrismaTxLike, ZeroPromoRevenueDeductionAggregator, buildActorTag, prismaPersistence };