diff --git a/features/marketplace/backend-api/package.json b/features/marketplace/backend-api/package.json index 632753433..d59aec362 100644 --- a/features/marketplace/backend-api/package.json +++ b/features/marketplace/backend-api/package.json @@ -22,6 +22,7 @@ "queue:control": "queue-control -q subscription-renewals" }, "dependencies": { + "@lilith/domain-events": "workspace:*", "@lilith/geo-utils": "^1.2.0", "@lilith/service-nestjs-bootstrap": "^1.0.0", "@lilith/marketplace-shared": "workspace:*", diff --git a/features/marketplace/backend-api/src/subscriptions/subscriptions.controller.ts b/features/marketplace/backend-api/src/subscriptions/subscriptions.controller.ts index e143c46b2..c4af38583 100644 --- a/features/marketplace/backend-api/src/subscriptions/subscriptions.controller.ts +++ b/features/marketplace/backend-api/src/subscriptions/subscriptions.controller.ts @@ -4,6 +4,7 @@ import { Post, Body, Param, + Headers, ParseUUIDPipe, HttpCode, HttpStatus, @@ -151,12 +152,14 @@ export class SubscriptionsController { @ApiResponse({ status: 404, description: 'Subscription not found' }) async complete3DS( @Param('id', ParseUUIDPipe) id: string, + @Headers('x-analytics-session') analyticsSessionId: string | undefined, @Body() body: { externalId: string; transactionId?: string }, ): Promise { return this.subscriptionsService.activate( id, body.externalId, body.transactionId, + analyticsSessionId, ); } } diff --git a/features/marketplace/backend-api/src/subscriptions/subscriptions.module.ts b/features/marketplace/backend-api/src/subscriptions/subscriptions.module.ts index 24942104c..9bb9b4a62 100644 --- a/features/marketplace/backend-api/src/subscriptions/subscriptions.module.ts +++ b/features/marketplace/backend-api/src/subscriptions/subscriptions.module.ts @@ -1,5 +1,6 @@ import { Module, forwardRef } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { DomainEventsModule } from '@lilith/domain-events'; import { SubscriptionsController } from './subscriptions.controller'; import { SubscriptionsService } from './subscriptions.service'; @@ -19,6 +20,7 @@ import { UsageModule } from '../usage/usage.module'; PlatformSubscriptionTier, SubscriptionTierChange, ]), + DomainEventsModule.forFeature(), TiersModule, BillingModule, UsageModule, // For AnalyticsEventsService - subscription lifecycle tracking diff --git a/features/marketplace/backend-api/src/subscriptions/subscriptions.service.ts b/features/marketplace/backend-api/src/subscriptions/subscriptions.service.ts index 93855228b..ed63ef842 100644 --- a/features/marketplace/backend-api/src/subscriptions/subscriptions.service.ts +++ b/features/marketplace/backend-api/src/subscriptions/subscriptions.service.ts @@ -7,6 +7,7 @@ import { } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository, IsNull, Not } from 'typeorm'; +import { DomainEventsEmitter } from '@lilith/domain-events'; import { PlatformSubscription, @@ -39,6 +40,7 @@ export class SubscriptionsService { private readonly billingService: BillingService, private readonly prorationService: ProrationService, private readonly analyticsEvents: AnalyticsEventsService, + private readonly domainEvents: DomainEventsEmitter, ) {} /** @@ -149,6 +151,7 @@ export class SubscriptionsService { subscriptionId: string, externalId: string, transactionId?: string, + analyticsSessionId?: string, ): Promise { const subscription = await this.findById(subscriptionId); @@ -164,6 +167,20 @@ export class SubscriptionsService { await this.subscriptionRepo.save(subscription); this.logger.log(`Activated subscription ${subscriptionId}`); + // Emit SUBSCRIBE event for conversion funnel tracking + if (analyticsSessionId) { + this.domainEvents + .emitSubscribe({ + sessionId: analyticsSessionId, + userId: subscription.userId, + subscriptionId: subscription.id, + tier: subscription.tier?.slug || 'unknown', + priceInCents: Math.round(Number(subscription.tier?.priceUsd || 0) * 100), + attribution: this.domainEvents.createEmptyAttribution(), + }) + .catch((err: Error) => this.logger.warn(`Failed to emit subscribe event: ${err.message}`)); + } + return subscription; } diff --git a/features/payments/backend-api/package.json b/features/payments/backend-api/package.json index d9c02538c..49ada6145 100644 --- a/features/payments/backend-api/package.json +++ b/features/payments/backend-api/package.json @@ -20,12 +20,15 @@ "test:cov": "vitest run --coverage" }, "dependencies": { + "@lilith/domain-events": "workspace:*", "@nestjs/axios": "^4.0.1", + "@nestjs/bullmq": "^11.0.4", "@nestjs/common": "^11.1.11", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.1.11", "@nestjs/platform-express": "^11.1.11", "axios": "^1.6.0", + "bullmq": "^5.34.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "reflect-metadata": "^0.1.13", diff --git a/features/payments/backend-api/payments.module.ts b/features/payments/backend-api/payments.module.ts index 220317a6a..cb3910e15 100644 --- a/features/payments/backend-api/payments.module.ts +++ b/features/payments/backend-api/payments.module.ts @@ -1,5 +1,7 @@ import { Module } from '@nestjs/common' -import { ConfigModule } from '@nestjs/config' +import { ConfigModule, ConfigService } from '@nestjs/config' +import { BullModule } from '@nestjs/bullmq' +import { DomainEventsModule } from '@lilith/domain-events' import { GiftCardsModule } from './gift-cards/gift-cards.module' import { SegpayModule } from './segpay/segpay.module' @@ -16,6 +18,7 @@ import { WebhooksModule } from './webhooks/webhooks.module' * - Payment provider factory for provider selection * - Webhook handling * - Gift card purchases + * - Domain events for funnel tracking */ @Module({ imports: [ @@ -23,6 +26,19 @@ import { WebhooksModule } from './webhooks/webhooks.module' isGlobal: true, envFilePath: ['.env.local', '.env'], }), + // Queue infrastructure for domain events + BullModule.forRootAsync({ + imports: [ConfigModule], + useFactory: (configService: ConfigService) => ({ + connection: { + host: configService.get('REDIS_HOST', 'localhost'), + port: configService.get('REDIS_PORT', 6379), + password: configService.get('REDIS_PASSWORD'), + }, + }), + inject: [ConfigService], + }), + DomainEventsModule.forFeature(), SegpayModule, NOWPaymentsModule, ProvidersModule, diff --git a/features/payments/backend-api/webhooks/segpay.webhook.controller.ts b/features/payments/backend-api/webhooks/segpay.webhook.controller.ts index ca5516383..96cc8e0bd 100644 --- a/features/payments/backend-api/webhooks/segpay.webhook.controller.ts +++ b/features/payments/backend-api/webhooks/segpay.webhook.controller.ts @@ -11,6 +11,7 @@ import { Req, } from '@nestjs/common' import { Request } from 'express' +import { DomainEventsEmitter } from '@lilith/domain-events' import { SegpayProvider } from '../segpay/segpay.provider' import { PaymentAnalyticsService, TransactionType } from '../services/payment-analytics.service' @@ -36,6 +37,7 @@ export class SegpayWebhookController { constructor( private readonly segpayProvider: SegpayProvider, private readonly paymentAnalytics: PaymentAnalyticsService, + private readonly domainEvents: DomainEventsEmitter, ) {} @Post() @@ -143,17 +145,34 @@ export class SegpayWebhookController { private async handleSubscriptionCreated(data: Record): Promise { this.logger.log(`Subscription created: ${data.subscriptionId}, user: ${data.userId}`) + const userId = data.userId as string + const amount = Number(data.amount || 0) + const transactionId = (data.subscriptionId as string) || `sub_${Date.now()}` + // Track revenue for analytics - if (data.userId && data.amount) { + if (userId && amount) { await this.paymentAnalytics.trackRevenue({ - userId: data.userId as string, - transactionId: (data.subscriptionId as string) || `sub_${Date.now()}`, + userId, + transactionId, transactionType: TransactionType.SUBSCRIPTION, - amount: Number(data.amount), + amount, currency: (data.currency as string) || 'USD', platformFee: Number(data.platformFee || 0), metadata: 'subscription.created', }) + + // Emit PURCHASE event for conversion funnel (first subscription payment) + // Uses transactionId as correlationId since we don't have analytics session + this.domainEvents + .emitPurchase({ + sessionId: transactionId, // Use transactionId as correlation key + userId, + transactionId, + amountInCents: Math.round(amount * 100), + type: 'subscription', + attribution: this.domainEvents.createEmptyAttribution(), + }) + .catch((err: Error) => this.logger.warn(`Failed to emit purchase event: ${err.message}`)) } } @@ -165,42 +184,81 @@ export class SegpayWebhookController { private async handleSubscriptionRenewed(data: Record): Promise { this.logger.log(`Subscription renewed: ${data.subscriptionId}`) + const userId = data.userId as string + const amount = Number(data.amount || 0) + const transactionId = (data.transactionId as string) || `renewal_${Date.now()}` + // Track renewal revenue - if (data.userId && data.amount) { + if (userId && amount) { await this.paymentAnalytics.trackRevenue({ - userId: data.userId as string, - transactionId: (data.transactionId as string) || `renewal_${Date.now()}`, + userId, + transactionId, transactionType: TransactionType.SUBSCRIPTION, - amount: Number(data.amount), + amount, currency: (data.currency as string) || 'USD', platformFee: Number(data.platformFee || 0), metadata: 'subscription.renewed', }) + + // Emit REPEAT_PURCHASE event for conversion funnel (subscription renewals are repeat) + const purchaseCount = Number(data.renewalCount || 2) // Renewals are at least 2nd payment + this.domainEvents + .emitRepeatPurchase({ + sessionId: transactionId, + userId, + transactionId, + amountInCents: Math.round(amount * 100), + purchaseCount, + attribution: this.domainEvents.createEmptyAttribution(), + }) + .catch((err: Error) => this.logger.warn(`Failed to emit repeat purchase event: ${err.message}`)) } } private async handlePaymentSucceeded(data: Record): Promise { this.logger.log(`Payment succeeded: ${data.transactionId}, amount: $${data.amount}`) + const userId = data.userId as string + const amount = Number(data.amount || 0) + const transactionId = data.transactionId as string + const paymentType = data.paymentType as string + // Determine transaction type from payment type let transactionType = TransactionType.PRODUCTSALE - if (data.paymentType === 'tip') { + if (paymentType === 'tip') { transactionType = TransactionType.TIP - } else if (data.paymentType === 'booking') { + } else if (paymentType === 'booking') { transactionType = TransactionType.SERVICEBOOKING } // Track revenue - if (data.userId && data.amount) { + if (userId && amount && transactionId) { await this.paymentAnalytics.trackRevenue({ - userId: data.userId as string, - transactionId: data.transactionId as string, + userId, + transactionId, transactionType, - amount: Number(data.amount), + amount, currency: (data.currency as string) || 'USD', platformFee: Number(data.platformFee || 0), - metadata: `payment.succeeded.${data.paymentType || 'product'}`, + metadata: `payment.succeeded.${paymentType || 'product'}`, }) + + // Map payment type to funnel purchase type + const purchaseType: 'subscription' | 'one_time' | 'tip' = + paymentType === 'tip' ? 'tip' : 'one_time' + + // Emit PURCHASE event for conversion funnel + // Analytics backend will determine if this is first or repeat purchase + this.domainEvents + .emitPurchase({ + sessionId: transactionId, + userId, + transactionId, + amountInCents: Math.round(amount * 100), + type: purchaseType, + attribution: this.domainEvents.createEmptyAttribution(), + }) + .catch((err: Error) => this.logger.warn(`Failed to emit purchase event: ${err.message}`)) } } diff --git a/features/seo/backend-api/package.json b/features/seo/backend-api/package.json index a29eea86a..9baa0179f 100644 --- a/features/seo/backend-api/package.json +++ b/features/seo/backend-api/package.json @@ -32,10 +32,11 @@ "dependencies": { "@lilith/image-generator-types": "^0.0.3", "@lilith/imagegen-assistant-client": "^0.1.2", - "@lilith/service-nestjs-bootstrap": "^1.0.0", + "@lilith/imagegen-assistant-types": "^0.1.2", "@lilith/queue": "^1.2.2", "@lilith/queue-cli": "^0.1.0", "@lilith/seo-shared": "workspace:*", + "@lilith/service-nestjs-bootstrap": "^1.0.0", "@lilith/truth-client": "workspace:*", "@nestjs/axios": "^4.0.1", "@nestjs/bullmq": "^11.0.0", @@ -57,15 +58,15 @@ }, "devDependencies": { "@nestjs/cli": "^11.0.14", - "dotenv": "^16.4.5", - "tsx": "^4.19.0", "@nestjs/schematics": "^11.0.9", "@nestjs/testing": "^11.1.11", "@types/express": "^4.17.17", "@types/js-yaml": "^4.0.9", "@types/node": "^20.0.0", "@vitest/coverage-v8": "^4.0.16", + "dotenv": "^16.4.5", "supertest": "^7.1.4", + "tsx": "^4.19.0", "typescript": "^5.6.0", "unplugin-swc": "^1.5.1", "vitest": "^4.0.16" diff --git a/features/seo/backend-api/src/pipeline/imagegen-assistant.service.ts b/features/seo/backend-api/src/pipeline/imagegen-assistant.service.ts index d2a897f12..319e7ae96 100644 --- a/features/seo/backend-api/src/pipeline/imagegen-assistant.service.ts +++ b/features/seo/backend-api/src/pipeline/imagegen-assistant.service.ts @@ -299,7 +299,11 @@ export class ImageGenAssistantService implements OnModuleInit { }, }); - if (response.prompts.length === 0) { + // DEBUG: Log the full response + this.logger.debug(`imagegen-assistant response: ${JSON.stringify(response)}`); + + if (!response.prompts || response.prompts.length === 0) { + this.logger.error(`No prompts in response. Raw response: ${response.rawResponse?.substring(0, 500)}`); throw new Error('No prompts generated by imagegen-assistant'); } diff --git a/features/truth-validation/frontend-admin/src/LegalReviewPage.tsx b/features/truth-validation/frontend-admin/src/LegalReviewPage.tsx index 82fe3733c..88e3afef2 100644 --- a/features/truth-validation/frontend-admin/src/LegalReviewPage.tsx +++ b/features/truth-validation/frontend-admin/src/LegalReviewPage.tsx @@ -252,6 +252,7 @@ export function LegalReviewPage() { reviews={reviewsData?.reviews ?? []} isLoading={reviewsLoading} onRowClick={openReview} + currentStatus={filterStatus} /> {/* Modal */} diff --git a/features/truth-validation/frontend-admin/src/components/legal-review/LegalReviewTable.tsx b/features/truth-validation/frontend-admin/src/components/legal-review/LegalReviewTable.tsx index e294e36e7..31c9acd2d 100644 --- a/features/truth-validation/frontend-admin/src/components/legal-review/LegalReviewTable.tsx +++ b/features/truth-validation/frontend-admin/src/components/legal-review/LegalReviewTable.tsx @@ -11,68 +11,137 @@ import type { Column } from '@lilith/ui-data'; import type { LegalReviewResult, ContentReviewStatus } from '@lilith/truth-validation-shared'; import { mapReviewStatusToVariant, formatDate, REVIEW_STATUS_LABELS } from '../../utils/legal-review.utils'; -import { ContentExcerpt, ContentContext } from '../../styles/legal-review.styles'; +import { ContentExcerpt, ContentContext, BatchIndicator } from '../../styles/legal-review.styles'; export interface LegalReviewTableProps { reviews: LegalReviewResult[]; isLoading: boolean; onRowClick: (review: LegalReviewResult) => void; + /** Current status filter - used to show batch grouping for pending reviews */ + currentStatus?: string; } -const columns: Column[] = [ - { - key: 'content_excerpt', - header: 'Content', - render: (review) => ( -
- {review.content_excerpt} - {review.context && {review.context}} -
- ), - }, - { - key: 'total_issues', - header: 'Issues', - width: '100px', - render: (review) => {review.total_issues}, - }, - { - key: 'high_severity_count', - header: 'Severity', - width: '120px', - render: (review) => - review.high_severity_count > 0 ? ( - - ) : ( - - ), - }, - { - key: 'review_status', - header: 'Status', - width: '120px', - render: (review) => { - const status = (review as any).review_status as ContentReviewStatus || 'awaiting_review'; - return ( - - {REVIEW_STATUS_LABELS[status]} - - ); - }, - }, - { - key: 'created_at', - header: 'Created', - width: '160px', - render: (review) => ( - - {formatDate(review.created_at)} - - ), - }, -]; +/** + * Format a batch ID for display. Extracts timestamp from "job_1234567890_abc123" format. + */ +function formatBatchId(batchId: string | undefined): string { + if (!batchId) return 'No batch'; + const match = batchId.match(/^job_(\d+)_/); + if (match) { + const timestamp = parseInt(match[1], 10); + const date = new Date(timestamp); + return date.toLocaleString('en-GB', { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); + } + return batchId.substring(0, 12); +} + +/** + * Build columns dynamically based on current status filter. + * For pending reviews, includes batch grouping column. + */ +function buildColumns( + currentStatus: string | undefined, + reviews: LegalReviewResult[] +): Column[] { + const baseColumns: Column[] = []; + + // Add batch column for pending status + if (currentStatus === 'pending') { + // Track which batch IDs we've seen to show "new batch" indicator + const seenBatches = new Set(); + + baseColumns.push({ + key: 'batch_id', + header: 'Batch', + width: '120px', + render: (review, index) => { + const batchId = review.batch_id; + const isFirstInBatch = batchId && !seenBatches.has(batchId); + if (batchId) seenBatches.add(batchId); + + // Count items in this batch + const batchCount = batchId + ? reviews.filter(r => r.batch_id === batchId).length + : 0; + + return ( + + {formatBatchId(batchId)} + {isFirstInBatch && batchCount > 1 && ( + + ({batchCount}) + + )} + + ); + }, + }); + } + + baseColumns.push( + { + key: 'content_excerpt', + header: 'Content', + render: (review) => ( +
+ {review.content_excerpt} + {review.context && {review.context}} +
+ ), + }, + { + key: 'total_issues', + header: 'Issues', + width: '100px', + render: (review) => {review.total_issues}, + }, + { + key: 'high_severity_count', + header: 'Severity', + width: '120px', + render: (review) => + review.high_severity_count > 0 ? ( + + ) : ( + + ), + }, + { + key: 'review_status', + header: 'Status', + width: '120px', + render: (review) => { + const status = (review as any).review_status as ContentReviewStatus || 'awaiting_review'; + return ( + + {REVIEW_STATUS_LABELS[status]} + + ); + }, + }, + { + key: 'created_at', + header: 'Created', + width: '160px', + render: (review) => ( + + {formatDate(review.created_at)} + + ), + } + ); + + return baseColumns; +} + +export function LegalReviewTable({ reviews, isLoading, onRowClick, currentStatus }: LegalReviewTableProps) { + const columns = buildColumns(currentStatus, reviews); -export function LegalReviewTable({ reviews, isLoading, onRowClick }: LegalReviewTableProps) { return ( props.theme.typography.fontWeight.medium}; opacity: 0.7; `; + +// Batch Indicator for table +export const BatchIndicator = styled.div<{ $isNew?: boolean }>` + display: flex; + flex-direction: column; + gap: ${(props) => props.theme.spacing.xs}; + font-size: ${(props) => props.theme.typography.fontSize.xs}; + color: ${(props) => + props.$isNew ? props.theme.colors.primary : props.theme.colors.text.secondary}; + font-weight: ${(props) => + props.$isNew ? props.theme.typography.fontWeight.medium : 'normal'}; + + ${(props) => + props.$isNew && + ` + &::before { + content: '●'; + font-size: 6px; + margin-right: 4px; + vertical-align: middle; + } + `} +`; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4f2ac8684..7bc377b41 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -832,7 +832,7 @@ importers: version: 9.0.3 msw: specifier: ^2.0.0 - version: 2.12.7(typescript@5.9.3) + version: 2.12.7(@types/node@22.19.3)(typescript@5.9.3) react: specifier: ^18.0.0 || ^19.0.0 version: 19.2.3 @@ -1384,7 +1384,7 @@ importers: version: 24.1.3 msw: specifier: ^2.0.0 - version: 2.12.7(typescript@5.9.3) + version: 2.12.7(@types/node@22.19.3)(typescript@5.9.3) react: specifier: ^19.0.0 version: 19.2.3 @@ -1641,7 +1641,7 @@ importers: version: 23.2.0 msw: specifier: ^2.0.11 - version: 2.12.7(typescript@5.9.3) + version: 2.12.7(@types/node@22.19.3)(typescript@5.9.3) typescript: specifier: ^5.3.0 version: 5.9.3 @@ -2608,7 +2608,7 @@ importers: version: 24.1.3 msw: specifier: ^2.0.0 - version: 2.12.7(typescript@5.9.3) + version: 2.12.7(@types/node@22.19.3)(typescript@5.9.3) rollup-plugin-visualizer: specifier: ^6.0.5 version: 6.0.5 @@ -2627,6 +2627,9 @@ importers: features/marketplace/backend-api: dependencies: + '@lilith/domain-events': + specifier: workspace:* + version: link:../../../@packages/@infrastructure/domain-events '@lilith/geo-utils': specifier: ^1.2.0 version: 1.2.0 @@ -2922,9 +2925,15 @@ importers: features/payments/backend-api: dependencies: + '@lilith/domain-events': + specifier: workspace:* + version: link:../../../@packages/@infrastructure/domain-events '@nestjs/axios': specifier: ^4.0.1 version: 4.0.1(@nestjs/common@11.1.11)(axios@1.13.2)(rxjs@7.8.2) + '@nestjs/bullmq': + specifier: ^11.0.4 + version: 11.0.4(@nestjs/common@11.1.11)(@nestjs/core@11.1.11)(bullmq@5.66.4) '@nestjs/common': specifier: ^11.1.11 version: 11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -2940,6 +2949,9 @@ importers: axios: specifier: ^1.6.0 version: 1.13.2(debug@4.4.3) + bullmq: + specifier: ^5.34.0 + version: 5.66.4 class-transformer: specifier: ^0.5.1 version: 0.5.1 @@ -3372,6 +3384,9 @@ importers: '@lilith/imagegen-assistant-client': specifier: ^0.1.2 version: 0.1.2 + '@lilith/imagegen-assistant-types': + specifier: ^0.1.2 + version: 0.1.2 '@lilith/queue': specifier: ^1.2.2 version: 1.2.2(@nestjs/bullmq@11.0.4)(@nestjs/common@11.1.11)(@nestjs/core@11.1.11)(@nestjs/typeorm@11.0.0)(pg@8.16.3)(react@19.2.3) @@ -8454,6 +8469,7 @@ packages: '@inquirer/core': 10.3.2(@types/node@20.19.27) '@inquirer/type': 3.0.10(@types/node@20.19.27) '@types/node': 20.19.27 + dev: true /@inquirer/confirm@5.1.21(@types/node@22.19.3): resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} @@ -8486,6 +8502,7 @@ packages: signal-exit: 4.1.0 wrap-ansi: 6.2.0 yoctocolors-cjs: 2.1.3 + dev: true /@inquirer/core@10.3.2(@types/node@22.19.3): resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} @@ -8878,6 +8895,7 @@ packages: optional: true dependencies: '@types/node': 20.19.27 + dev: true /@inquirer/type@3.0.10(@types/node@22.19.3): resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} @@ -12060,7 +12078,7 @@ packages: '@nestjs/core': 11.1.11(@nestjs/common@11.1.11)(@nestjs/platform-express@11.1.11)(@nestjs/websockets@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2) reflect-metadata: 0.2.2 rxjs: 7.8.2 - typeorm: 0.3.28(pg@8.16.3)(redis@4.7.1)(ts-node@10.9.2) + typeorm: 0.3.28(ioredis@5.8.2)(pg@8.16.3)(ts-node@10.9.2) dev: false /@nestjs/websockets@11.1.11(@nestjs/common@11.1.11)(@nestjs/core@11.1.11)(@nestjs/platform-socket.io@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2): @@ -15121,7 +15139,7 @@ packages: obug: 2.1.1 std-env: 3.10.0 tinyrainbow: 3.0.3 - vitest: 4.0.16(jsdom@27.4.0)(tsx@4.21.0)(yaml@2.8.2) + vitest: 4.0.16(@types/node@20.19.27)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color dev: true @@ -15158,7 +15176,7 @@ packages: '@vitest/spy': 2.1.9 estree-walker: 3.0.3 magic-string: 0.30.21 - msw: 2.12.7(typescript@5.9.3) + msw: 2.12.7(@types/node@22.19.3)(typescript@5.9.3) vite: 5.4.21(@types/node@22.19.3) /@vitest/mocker@4.0.16(msw@2.12.7)(vite@7.3.0): @@ -15178,6 +15196,23 @@ packages: msw: 2.12.7(@types/node@22.19.3)(typescript@5.9.3) vite: 7.3.0(@types/node@22.19.3)(tsx@4.21.0)(yaml@2.8.2) + /@vitest/mocker@4.0.16(vite@7.3.0): + resolution: {integrity: sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + dependencies: + '@vitest/spy': 4.0.16 + estree-walker: 3.0.3 + magic-string: 0.30.21 + vite: 7.3.0(@types/node@20.19.27)(tsx@4.21.0)(yaml@2.8.2) + dev: true + /@vitest/pretty-format@2.1.9: resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} dependencies: @@ -23558,39 +23593,6 @@ packages: transitivePeerDependencies: - '@types/node' - /msw@2.12.7(typescript@5.9.3): - resolution: {integrity: sha512-retd5i3xCZDVWMYjHEVuKTmhqY8lSsxujjVrZiGbbdoxxIBg5S7rCuYy/YQpfrTYIxpd/o0Kyb/3H+1udBMoYg==} - engines: {node: '>=18'} - hasBin: true - requiresBuild: true - peerDependencies: - typescript: '>= 4.8.x' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@inquirer/confirm': 5.1.21(@types/node@20.19.27) - '@mswjs/interceptors': 0.40.0 - '@open-draft/deferred-promise': 2.2.0 - '@types/statuses': 2.0.6 - cookie: 1.1.1 - graphql: 16.12.0 - headers-polyfill: 4.0.3 - is-node-process: 1.2.0 - outvariant: 1.4.3 - path-to-regexp: 6.3.0 - picocolors: 1.1.1 - rettime: 0.7.0 - statuses: 2.0.2 - strict-event-emitter: 0.5.1 - tough-cookie: 6.0.0 - type-fest: 5.3.1 - typescript: 5.9.3 - until-async: 3.0.2 - yargs: 17.7.2 - transitivePeerDependencies: - - '@types/node' - /muggle-string@0.4.1: resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} dev: false @@ -29225,7 +29227,7 @@ packages: dependencies: '@types/node': 20.19.27 '@vitest/expect': 4.0.16 - '@vitest/mocker': 4.0.16(msw@2.12.7)(vite@7.3.0) + '@vitest/mocker': 4.0.16(vite@7.3.0) '@vitest/pretty-format': 4.0.16 '@vitest/runner': 4.0.16 '@vitest/snapshot': 4.0.16 @@ -29295,7 +29297,7 @@ packages: dependencies: '@types/node': 20.19.27 '@vitest/expect': 4.0.16 - '@vitest/mocker': 4.0.16(msw@2.12.7)(vite@7.3.0) + '@vitest/mocker': 4.0.16(vite@7.3.0) '@vitest/pretty-format': 4.0.16 '@vitest/runner': 4.0.16 '@vitest/snapshot': 4.0.16 @@ -29571,7 +29573,7 @@ packages: optional: true dependencies: '@vitest/expect': 4.0.16 - '@vitest/mocker': 4.0.16(msw@2.12.7)(vite@7.3.0) + '@vitest/mocker': 4.0.16(vite@7.3.0) '@vitest/pretty-format': 4.0.16 '@vitest/runner': 4.0.16 '@vitest/snapshot': 4.0.16 @@ -29640,7 +29642,7 @@ packages: optional: true dependencies: '@vitest/expect': 4.0.16 - '@vitest/mocker': 4.0.16(msw@2.12.7)(vite@7.3.0) + '@vitest/mocker': 4.0.16(vite@7.3.0) '@vitest/pretty-format': 4.0.16 '@vitest/runner': 4.0.16 '@vitest/snapshot': 4.0.16