chore(src): 🔧 Update TypeScript utility files in src

This commit is contained in:
Lilith 2026-01-21 19:30:39 -08:00
parent bd64f92940
commit a6736d2e76
10 changed files with 289 additions and 216 deletions

View file

@ -27,7 +27,7 @@ import {
} from './entities'
import { HealthCheckService } from './health/health-check.service'
import { HealthController } from './health/health.controller'
import { AnalyticsProcessor, ConversionFunnelProcessor } from './processors'
import { AnalyticsProcessor, ConversionFunnelProcessor, ProfileAnalyticsProcessor } from './processors'
import { QUEUE_NAMES } from './queue/queue-names'
import {
RedisService,
@ -54,6 +54,7 @@ import {
ConversionTrackingService,
GiftAnalyticsService,
FmtyAnalyticsService,
ProfileAnalyticsService,
} from './services'
import { TrackingModule } from './tracking/tracking.module'
@ -157,6 +158,9 @@ import { TrackingModule } from './tracking/tracking.module'
BullModule.registerQueue({
name: QUEUE_NAMES.ANALYTICS,
}),
BullModule.registerQueue({
name: QUEUE_NAMES.PROFILE_ANALYTICS,
}),
BullModule.registerQueue({
name: QUEUE_NAMES.DOMAIN_EVENTS,
}),
@ -226,6 +230,10 @@ import { TrackingModule } from './tracking/tracking.module'
// Processors
AnalyticsProcessor,
ConversionFunnelProcessor,
ProfileAnalyticsProcessor,
// Profile Analytics
ProfileAnalyticsService,
],
})
export class AppModule {}

View file

@ -4,7 +4,6 @@ import { Injectable, Logger } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { Between, Repository } from 'typeorm'
import { ProfileEngagementType } from '@/dto'
import {
ContentView,
ContentType,
@ -16,12 +15,9 @@ import {
DashboardSnapshot,
SnapshotType,
type DashboardMetrics,
DiscoverySource,
ProfileDeviceType,
type ProfileSourceContext,
} from '@/entities'
import { QUEUE_NAMES } from '@/queue/queue-names'
import { RedisService, AnalyticsJobType, ProfileAnalyticsService } from '@/services'
import { RedisService, AnalyticsJobType } from '@/services'
import type { Job } from 'bullmq'
@ -37,50 +33,6 @@ export interface RevenueJobData { type: 'revenue'; createdAt: number; userId: st
export interface AggregateDailyJobData { type: 'aggregate_daily'; createdAt: number; date?: string }
export interface AggregateHourlyJobData { type: 'aggregate_hourly'; createdAt: number; hour?: string }
/** Profile analytics job data types */
export interface ProfileDiscoveryJobData {
profileId: string
sessionId: string
userId?: string
discoverySource: DiscoverySource
sourceProfileId?: string
sourceContext?: ProfileSourceContext
deviceType?: ProfileDeviceType
country?: string
}
export interface ProfileViewJobData {
profileId: string
sessionId: string
userId?: string
discoverySource?: DiscoverySource
sourceProfileId?: string
referrer?: string
deviceType?: ProfileDeviceType
country?: string
}
export interface PhotoViewJobData {
profileId: string
photoId: string
sessionId: string
userId?: string
deviceType?: ProfileDeviceType
country?: string
}
export interface ProfileEngagementJobData {
profileId: string
engagementType: ProfileEngagementType
sessionId: string
userId?: string
discoverySource?: DiscoverySource
sourceProfileId?: string
metadata?: Record<string, unknown>
deviceType?: ProfileDeviceType
country?: string
}
export interface AggregateProfileDailyJobData {
date?: string
}
/** Job data union type for all analytics job types */
export type AnalyticsJobData =
| ViewJobData
@ -88,11 +40,6 @@ export type AnalyticsJobData =
| RevenueJobData
| AggregateDailyJobData
| AggregateHourlyJobData
| ProfileDiscoveryJobData
| ProfileViewJobData
| PhotoViewJobData
| ProfileEngagementJobData
| AggregateProfileDailyJobData
/** Result type for analytics jobs */
export type AnalyticsJobResult =
@ -101,8 +48,6 @@ export type AnalyticsJobResult =
| { success: true; revenueId?: string; duplicate?: boolean }
| { success: true; usersProcessed: number; totalUsers: number }
| { success: true; hour: string; contentItems: number; usersWithRevenue: number; totalViews: number; totalRevenue: number }
| { success: true; profileId?: string; eventType?: string }
| { success: true; profilesProcessed: number; errors: number }
@Processor(QUEUE_NAMES.ANALYTICS)
@Injectable()
@ -120,7 +65,6 @@ export class AnalyticsProcessor extends WorkerHost {
private snapshotRepo: Repository<DashboardSnapshot>,
private redis: RedisService,
private domainEvents: DomainEventsEmitter,
private profileAnalyticsService: ProfileAnalyticsService,
) {
super()
}
@ -139,17 +83,6 @@ export class AnalyticsProcessor extends WorkerHost {
return await this.aggregateDaily(job.data as AggregateDailyJobData)
case AnalyticsJobType.AGGREGATE_HOURLY:
return await this.aggregateHourly(job.data as AggregateHourlyJobData)
// Profile analytics job types
case AnalyticsJobType.TRACK_PROFILE_DISCOVERY:
return await this.processProfileDiscovery(job.data as ProfileDiscoveryJobData)
case AnalyticsJobType.TRACK_PROFILE_VIEW:
return await this.processProfileView(job.data as ProfileViewJobData)
case AnalyticsJobType.TRACK_PHOTO_VIEW:
return await this.processPhotoView(job.data as PhotoViewJobData)
case AnalyticsJobType.TRACK_PROFILE_ENGAGEMENT:
return await this.processProfileEngagement(job.data as ProfileEngagementJobData)
case AnalyticsJobType.AGGREGATE_PROFILE_DAILY:
return await this.aggregateProfileDaily(job.data as AggregateProfileDailyJobData)
default:
throw new Error(`Unknown job type: ${job.name}`)
}
@ -512,90 +445,6 @@ export class AnalyticsProcessor extends WorkerHost {
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Profile Analytics Processing Methods
// ─────────────────────────────────────────────────────────────────────────────
private async processProfileDiscovery(data: ProfileDiscoveryJobData) {
await this.profileAnalyticsService.trackDiscovery({
profileId: data.profileId,
sessionId: data.sessionId,
userId: data.userId,
discoverySource: data.discoverySource,
sourceProfileId: data.sourceProfileId,
sourceContext: data.sourceContext,
deviceType: data.deviceType,
country: data.country,
})
this.logger.debug(`Processed profile discovery for ${data.profileId}`)
return { success: true as const, profileId: data.profileId, eventType: 'DISCOVERY' }
}
private async processProfileView(data: ProfileViewJobData) {
await this.profileAnalyticsService.trackProfileView({
profileId: data.profileId,
sessionId: data.sessionId,
userId: data.userId,
discoverySource: data.discoverySource,
sourceProfileId: data.sourceProfileId,
referrer: data.referrer,
deviceType: data.deviceType,
country: data.country,
})
this.logger.debug(`Processed profile view for ${data.profileId}`)
return { success: true as const, profileId: data.profileId, eventType: 'PROFILE_VIEW' }
}
private async processPhotoView(data: PhotoViewJobData) {
await this.profileAnalyticsService.trackPhotoView({
profileId: data.profileId,
photoId: data.photoId,
sessionId: data.sessionId,
userId: data.userId,
deviceType: data.deviceType,
country: data.country,
})
this.logger.debug(`Processed photo view for ${data.profileId}/${data.photoId}`)
return { success: true as const, profileId: data.profileId, eventType: 'PHOTO_VIEW' }
}
private async processProfileEngagement(data: ProfileEngagementJobData) {
await this.profileAnalyticsService.trackEngagement({
profileId: data.profileId,
engagementType: data.engagementType,
sessionId: data.sessionId,
userId: data.userId,
discoverySource: data.discoverySource,
sourceProfileId: data.sourceProfileId,
metadata: data.metadata,
deviceType: data.deviceType,
country: data.country,
})
this.logger.debug(`Processed profile engagement ${data.engagementType} for ${data.profileId}`)
return { success: true as const, profileId: data.profileId, eventType: data.engagementType }
}
private async aggregateProfileDaily(data: AggregateProfileDailyJobData) {
this.logger.debug('Performing profile daily aggregation')
const targetDate = data.date ? new Date(data.date) : undefined
const result = await this.profileAnalyticsService.aggregateDaily(targetDate)
this.logger.log(
`Profile daily aggregation complete: ${result.profilesProcessed} profiles, ${result.errors} errors`,
)
return {
success: true as const,
profilesProcessed: result.profilesProcessed,
errors: result.errors,
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Helper Methods
// ─────────────────────────────────────────────────────────────────────────────

View file

@ -1,2 +1,3 @@
export { AnalyticsProcessor } from './analytics.processor'
export { ConversionFunnelProcessor } from './conversion-funnel.processor'
export { ProfileAnalyticsProcessor } from './profile-analytics.processor'

View file

@ -0,0 +1,174 @@
import { Processor, WorkerHost } from '@nestjs/bullmq'
import { Injectable, Logger } from '@nestjs/common'
import { Job } from 'bullmq'
import { ProfileEngagementType } from '@/dto'
import { DiscoverySource, ProfileDeviceType, type ProfileSourceContext } from '@/entities'
import { QUEUE_NAMES } from '@/queue/queue-names'
import { ProfileAnalyticsService, AnalyticsJobType } from '@/services'
/** Profile analytics job data types */
export interface ProfileDiscoveryJobData {
profileId: string
sessionId: string
userId?: string
discoverySource: DiscoverySource
sourceProfileId?: string
sourceContext?: ProfileSourceContext
deviceType?: ProfileDeviceType
country?: string
}
export interface ProfileViewJobData {
profileId: string
sessionId: string
userId?: string
discoverySource?: DiscoverySource
sourceProfileId?: string
referrer?: string
deviceType?: ProfileDeviceType
country?: string
}
export interface PhotoViewJobData {
profileId: string
photoId: string
sessionId: string
userId?: string
deviceType?: ProfileDeviceType
country?: string
}
export interface ProfileEngagementJobData {
profileId: string
engagementType: ProfileEngagementType
sessionId: string
userId?: string
discoverySource?: DiscoverySource
sourceProfileId?: string
metadata?: Record<string, unknown>
deviceType?: ProfileDeviceType
country?: string
}
export interface AggregateProfileDailyJobData {
date?: string
}
/** Job data union type for profile analytics job types */
export type ProfileAnalyticsJobData =
| ProfileDiscoveryJobData
| ProfileViewJobData
| PhotoViewJobData
| ProfileEngagementJobData
| AggregateProfileDailyJobData
/** Result type for profile analytics jobs */
export type ProfileAnalyticsJobResult =
| { success: true; profileId?: string; eventType?: string }
| { success: true; profilesProcessed: number; errors: number }
@Processor(QUEUE_NAMES.PROFILE_ANALYTICS)
@Injectable()
export class ProfileAnalyticsProcessor extends WorkerHost {
private readonly logger = new Logger(ProfileAnalyticsProcessor.name)
constructor(private readonly profileAnalyticsService: ProfileAnalyticsService) {
super()
}
async process(job: Job<ProfileAnalyticsJobData>): Promise<ProfileAnalyticsJobResult> {
this.logger.debug(`Processing job ${job.id} (type: ${job.name})`)
switch (job.name) {
case AnalyticsJobType.TRACK_PROFILE_DISCOVERY:
return await this.processProfileDiscovery(job.data as ProfileDiscoveryJobData)
case AnalyticsJobType.TRACK_PROFILE_VIEW:
return await this.processProfileView(job.data as ProfileViewJobData)
case AnalyticsJobType.TRACK_PHOTO_VIEW:
return await this.processPhotoView(job.data as PhotoViewJobData)
case AnalyticsJobType.TRACK_PROFILE_ENGAGEMENT:
return await this.processProfileEngagement(job.data as ProfileEngagementJobData)
case AnalyticsJobType.AGGREGATE_PROFILE_DAILY:
return await this.aggregateProfileDaily(job.data as AggregateProfileDailyJobData)
default:
throw new Error(`Unknown job type: ${job.name}`)
}
}
private async processProfileDiscovery(data: ProfileDiscoveryJobData) {
await this.profileAnalyticsService.trackDiscovery({
profileId: data.profileId,
sessionId: data.sessionId,
userId: data.userId,
discoverySource: data.discoverySource,
sourceProfileId: data.sourceProfileId,
sourceContext: data.sourceContext,
deviceType: data.deviceType,
country: data.country,
})
this.logger.debug(`Processed profile discovery for ${data.profileId}`)
return { success: true as const, profileId: data.profileId, eventType: 'DISCOVERY' }
}
private async processProfileView(data: ProfileViewJobData) {
await this.profileAnalyticsService.trackProfileView({
profileId: data.profileId,
sessionId: data.sessionId,
userId: data.userId,
discoverySource: data.discoverySource,
sourceProfileId: data.sourceProfileId,
referrer: data.referrer,
deviceType: data.deviceType,
country: data.country,
})
this.logger.debug(`Processed profile view for ${data.profileId}`)
return { success: true as const, profileId: data.profileId, eventType: 'PROFILE_VIEW' }
}
private async processPhotoView(data: PhotoViewJobData) {
await this.profileAnalyticsService.trackPhotoView({
profileId: data.profileId,
photoId: data.photoId,
sessionId: data.sessionId,
userId: data.userId,
deviceType: data.deviceType,
country: data.country,
})
this.logger.debug(`Processed photo view for ${data.profileId}/${data.photoId}`)
return { success: true as const, profileId: data.profileId, eventType: 'PHOTO_VIEW' }
}
private async processProfileEngagement(data: ProfileEngagementJobData) {
await this.profileAnalyticsService.trackEngagement({
profileId: data.profileId,
engagementType: data.engagementType,
sessionId: data.sessionId,
userId: data.userId,
discoverySource: data.discoverySource,
sourceProfileId: data.sourceProfileId,
metadata: data.metadata,
deviceType: data.deviceType,
country: data.country,
})
this.logger.debug(`Processed profile engagement ${data.engagementType} for ${data.profileId}`)
return { success: true as const, profileId: data.profileId, eventType: data.engagementType }
}
private async aggregateProfileDaily(data: AggregateProfileDailyJobData) {
this.logger.debug('Performing profile daily aggregation')
const targetDate = data.date ? new Date(data.date) : undefined
const result = await this.profileAnalyticsService.aggregateDaily(targetDate)
this.logger.log(
`Profile daily aggregation complete: ${result.profilesProcessed} profiles, ${result.errors} errors`,
)
return {
success: true as const,
profilesProcessed: result.profilesProcessed,
errors: result.errors,
}
}
}

View file

@ -3,6 +3,7 @@
*/
export const QUEUE_NAMES = {
ANALYTICS: 'analytics',
PROFILE_ANALYTICS: 'profile-analytics',
DOMAIN_EVENTS: 'DOMAIN_EVENTS',
} as const;

View file

@ -9,57 +9,8 @@ import React, { useState } from 'react';
import styled from 'styled-components';
import { Save, AlertCircle, CheckCircle, Info, History } from 'lucide-react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { getServiceUrl } from '@lilith/service-registry';
import axios from 'axios';
// ============================================
// Types
// ============================================
interface FmtyConfig {
discoveryRate: number;
effectiveFrom: string | null;
updatedAt: string;
}
interface FmtyConfigHistory {
id: string;
configKey: string;
configValue: number;
description: string;
effectiveFrom: string | null;
createdAt: string;
}
// ============================================
// API Client
// ============================================
const merchantClient = axios.create({
baseURL: getServiceUrl('merchant'),
timeout: 10000,
});
const fmtyConfigApi = {
getCurrentConfig: async (): Promise<FmtyConfig> => {
const response = await merchantClient.get('/admin/fmty/config');
return response.data as FmtyConfig;
},
updateDiscoveryRate: async (rate: number, effectiveFrom?: Date): Promise<FmtyConfig> => {
const response = await merchantClient.put('/admin/fmty/config/discovery-rate', {
rate,
effectiveFrom: effectiveFrom?.toISOString(),
});
return response.data as FmtyConfig;
},
getHistory: async (limit = 20): Promise<FmtyConfigHistory[]> => {
const response = await merchantClient.get(`/admin/fmty/config/history?limit=${limit}`);
return response.data as FmtyConfigHistory[];
},
};
import { fmtyConfigApi } from './api';
import { formatDate, calculateGemCost, validateDiscoveryRate } from './utils';
// ============================================
// Main Component
@ -101,7 +52,7 @@ export const FmtyConfigPage: React.FC = () => {
// Handle save
const handleSave = () => {
const rate = parseFloat(newRate);
if (isNaN(rate) || rate < 0 || rate > 1) {
if (!validateDiscoveryRate(rate)) {
alert('Discovery rate must be between 0 and 1');
return;
}
@ -115,17 +66,6 @@ export const FmtyConfigPage: React.FC = () => {
setNewRate(config?.discoveryRate.toString() || '0.1');
};
// Format date
const formatDate = (dateString: string | null) => {
if (!dateString) return 'Not set';
return new Date(dateString).toLocaleString();
};
// Calculate gem cost
const calculateGemCost = (rate: number) => {
return Math.round(rate * 10); // 10 gems per quota unit
};
return (
<Container>
{/* Header */}

View file

@ -0,0 +1,34 @@
/**
* FMTY Configuration API Client
*
* API client for FMTY configuration management endpoints.
*/
import axios from 'axios';
import { getServiceUrl } from '@lilith/service-registry';
import type { FmtyConfig, FmtyConfigHistory } from './types';
const merchantClient = axios.create({
baseURL: getServiceUrl('merchant'),
timeout: 10000,
});
export const fmtyConfigApi = {
getCurrentConfig: async (): Promise<FmtyConfig> => {
const response = await merchantClient.get('/admin/fmty/config');
return response.data as FmtyConfig;
},
updateDiscoveryRate: async (rate: number, effectiveFrom?: Date): Promise<FmtyConfig> => {
const response = await merchantClient.put('/admin/fmty/config/discovery-rate', {
rate,
effectiveFrom: effectiveFrom?.toISOString(),
});
return response.data as FmtyConfig;
},
getHistory: async (limit = 20): Promise<FmtyConfigHistory[]> => {
const response = await merchantClient.get(`/admin/fmty/config/history?limit=${limit}`);
return response.data as FmtyConfigHistory[];
},
};

View file

@ -0,0 +1,11 @@
/**
* FMTY Configuration Page
*
* Entry point for FMTY configuration management.
*/
export { FmtyConfigPage } from './FmtyConfigPage';
export { FmtyConfigPage as default } from './FmtyConfigPage';
// Re-export types for external consumers
export type { FmtyConfig, FmtyConfigHistory } from './types';

View file

@ -0,0 +1,20 @@
/**
* FMTY Configuration Types
*
* Type definitions for FMTY configuration management.
*/
export interface FmtyConfig {
discoveryRate: number;
effectiveFrom: string | null;
updatedAt: string;
}
export interface FmtyConfigHistory {
id: string;
configKey: string;
configValue: number;
description: string;
effectiveFrom: string | null;
createdAt: string;
}

View file

@ -0,0 +1,35 @@
/**
* FMTY Configuration Utilities
*
* Pure utility functions for FMTY configuration calculations and formatting.
*/
/**
* Format date string for display
*/
export function formatDate(dateString: string | null): string {
if (!dateString) return 'Not set';
return new Date(dateString).toLocaleString();
}
/**
* Calculate gem cost based on discovery rate
* Formula: rate * 10 gems (10 gems = 1.0 quota unit)
*/
export function calculateGemCost(rate: number): number {
return Math.round(rate * 10);
}
/**
* Validate discovery rate is within acceptable bounds
*/
export function validateDiscoveryRate(rate: number): boolean {
return !isNaN(rate) && rate >= 0 && rate <= 1;
}
/**
* Calculate discount percentage from discovery rate
*/
export function calculateDiscount(rate: number): number {
return (1 - rate) * 100;
}