chore(src): 🔧 Update TypeScript utility files in src
This commit is contained in:
parent
bd64f92940
commit
a6736d2e76
10 changed files with 289 additions and 216 deletions
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
export { AnalyticsProcessor } from './analytics.processor'
|
||||
export { ConversionFunnelProcessor } from './conversion-funnel.processor'
|
||||
export { ProfileAnalyticsProcessor } from './profile-analytics.processor'
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
*/
|
||||
export const QUEUE_NAMES = {
|
||||
ANALYTICS: 'analytics',
|
||||
PROFILE_ANALYTICS: 'profile-analytics',
|
||||
DOMAIN_EVENTS: 'DOMAIN_EVENTS',
|
||||
} as const;
|
||||
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
|
|
@ -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[];
|
||||
},
|
||||
};
|
||||
|
|
@ -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';
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue