import db from '../config/database'; import { logger } from '../utils/logger'; import { v4 as uuidv4 } from 'uuid'; export interface ReturnRecord { id: string; tenant_id: string; shop_id: string; task_id?: string; trace_id: string; order_id: string; order_item_id: string; product_id: string; sku_id: string; platform: string; platform_product_id: string; platform_sku_id: string; buyer_id: string; return_id: string; return_reason: string; return_reason_category: string; return_quantity: number; refund_amount: decimal; return_status: 'PENDING' | 'APPROVED' | 'REJECTED' | 'COMPLETED'; return_type: 'FULL' | 'PARTIAL'; inspection_result?: string; is_accepted: boolean; restocking_fee?: decimal; return_shipping_fee?: decimal; created_at: Date; updated_at: Date; } export interface SKUReturnMetrics { id: string; tenant_id: string; shop_id: string; product_id: string; sku_id: string; platform: string; platform_product_id: string; platform_sku_id: string; total_orders: number; total_returns: number; return_rate: decimal; return_rate_trend: decimal; avg_refund_amount: decimal; total_refund_amount: decimal; return_by_reason: Record; return_by_month: Record; high_risk_threshold: decimal; is_high_risk: boolean; is_auto_delisted: boolean; last_calculated_at: Date; calculated_period_start: Date; calculated_period_end: Date; } export interface ReturnRateThreshold { id: string; tenant_id: string; shop_id?: string; platform?: string; category?: string; threshold_type: 'GLOBAL' | 'PLATFORM' | 'CATEGORY' | 'SKU'; return_rate_threshold: decimal; auto_delist_enabled: boolean; delist_duration_hours?: number; notification_enabled: boolean; notify_emails?: string[]; created_by: string; created_at: Date; updated_at: Date; } export interface ReturnAnalysis { product_id: string; sku_id: string; return_rate: decimal; top_return_reasons: string[]; customer_sentiment: 'POSITIVE' | 'NEUTRAL' | 'NEGATIVE'; improvement_priority: 'HIGH' | 'MEDIUM' | 'LOW'; estimated_impact: string; recommendations: string[]; } export default class ReturnRateDatabaseService { static readonly TABLE_NAME = 'cf_return_record'; static readonly SKU_METRICS_TABLE = 'cf_sku_return_metrics'; static readonly THRESHOLD_TABLE = 'cf_return_rate_threshold'; static async initTable(): Promise { const hasTable = await db.schema.hasTable(this.TABLE_NAME); if (!hasTable) { logger.info(`[ReturnRateDatabaseService] Creating ${this.TABLE_NAME} table...`); await db.schema.createTable(this.TABLE_NAME, (table) => { table.string('id', 36).primary(); table.string('tenant_id', 64).notNullable().index(); table.string('shop_id', 64).notNullable().index(); table.string('task_id', 36); table.string('trace_id', 64).notNullable(); table.string('order_id', 64).notNullable().index(); table.string('order_item_id', 64).notNullable(); table.string('product_id', 64).notNullable().index(); table.string('sku_id', 64).notNullable().index(); table.string('platform', 64).notNullable().index(); table.string('platform_product_id', 64).notNullable(); table.string('platform_sku_id', 64).notNullable(); table.string('buyer_id', 64).notNullable(); table.string('return_id', 64).notNullable().unique(); table.text('return_reason').notNullable(); table.string('return_reason_category', 64).notNullable(); table.integer('return_quantity').notNullable(); table.decimal('refund_amount', 10, 2).notNullable(); table.enum('return_status', ['PENDING', 'APPROVED', 'REJECTED', 'COMPLETED']).notNullable(); table.enum('return_type', ['FULL', 'PARTIAL']).notNullable(); table.text('inspection_result'); table.boolean('is_accepted').notNullable().defaultTo(false); table.decimal('restocking_fee', 10, 2); table.decimal('return_shipping_fee', 10, 2); table.datetime('created_at').notNullable().defaultTo(db.fn.now()); table.datetime('updated_at').notNullable().defaultTo(db.fn.now()); table.index(['tenant_id', 'shop_id']); table.index(['platform', 'platform_product_id']); table.index(['return_status', 'created_at']); }); logger.info(`[ReturnRateDatabaseService] Table ${this.TABLE_NAME} created`); } const hasMetricsTable = await db.schema.hasTable(this.SKU_METRICS_TABLE); if (!hasMetricsTable) { logger.info(`[ReturnRateDatabaseService] Creating ${this.SKU_METRICS_TABLE} table...`); await db.schema.createTable(this.SKU_METRICS_TABLE, (table) => { table.string('id', 36).primary(); table.string('tenant_id', 64).notNullable().index(); table.string('shop_id', 64).notNullable().index(); table.string('product_id', 64).notNullable().index(); table.string('sku_id', 64).notNullable().index(); table.string('platform', 64).notNullable().index(); table.string('platform_product_id', 64).notNullable(); table.string('platform_sku_id', 64).notNullable(); table.integer('total_orders').notNullable().defaultTo(0); table.integer('total_returns').notNullable().defaultTo(0); table.decimal('return_rate', 5, 2).notNullable().defaultTo(0); table.decimal('return_rate_trend', 5, 2).notNullable().defaultTo(0); table.decimal('avg_refund_amount', 10, 2).notNullable().defaultTo(0); table.decimal('total_refund_amount', 10, 2).notNullable().defaultTo(0); table.json('return_by_reason').notNullable(); table.json('return_by_month').notNullable(); table.decimal('high_risk_threshold', 5, 2).notNullable().defaultTo(30); table.boolean('is_high_risk').notNullable().defaultTo(false); table.boolean('is_auto_delisted').notNullable().defaultTo(false); table.datetime('last_calculated_at').notNullable(); table.datetime('calculated_period_start').notNullable(); table.datetime('calculated_period_end').notNullable(); table.index(['tenant_id', 'shop_id', 'platform']); table.index(['return_rate', 'is_high_risk']); table.index(['is_auto_delisted']); }); logger.info(`[ReturnRateDatabaseService] Table ${this.SKU_METRICS_TABLE} created`); } const hasThresholdTable = await db.schema.hasTable(this.THRESHOLD_TABLE); if (!hasThresholdTable) { logger.info(`[ReturnRateDatabaseService] Creating ${this.THRESHOLD_TABLE} table...`); await db.schema.createTable(this.THRESHOLD_TABLE, (table) => { table.string('id', 36).primary(); table.string('tenant_id', 64).notNullable().index(); table.string('shop_id', 64); table.string('platform', 64); table.string('category', 64); table.enum('threshold_type', ['GLOBAL', 'PLATFORM', 'CATEGORY', 'SKU']).notNullable(); table.decimal('return_rate_threshold', 5, 2).notNullable(); table.boolean('auto_delist_enabled').notNullable().defaultTo(true); table.integer('delist_duration_hours'); table.boolean('notification_enabled').notNullable().defaultTo(true); table.json('notify_emails'); table.string('created_by', 64).notNullable(); table.datetime('created_at').notNullable().defaultTo(db.fn.now()); table.datetime('updated_at').notNullable().defaultTo(db.fn.now()); table.unique(['tenant_id', 'threshold_type', 'platform', 'category']); }); logger.info(`[ReturnRateDatabaseService] Table ${this.THRESHOLD_TABLE} created`); } } static async createReturnRecord(record: Omit): Promise { const id = uuidv4(); const now = new Date(); const newRecord: ReturnRecord = { ...record, id, created_at: now, updated_at: now }; await db(this.TABLE_NAME).insert(newRecord); logger.info(`[ReturnRateDatabaseService] Return record created: id=${id}, orderId=${record.order_id}, traceId=${record.trace_id}`); return newRecord; } static async getReturnRecords(filter: { tenant_id?: string; shop_id?: string; product_id?: string; sku_id?: string; platform?: string; return_status?: string; start_date?: Date; end_date?: Date; }, limit: number = 100, offset: number = 0): Promise<{ records: ReturnRecord[]; total: number }> { let query = db(this.TABLE_NAME); if (filter.tenant_id) query = query.where({ tenant_id: filter.tenant_id }); if (filter.shop_id) query = query.where({ shop_id: filter.shop_id }); if (filter.product_id) query = query.where({ product_id: filter.product_id }); if (filter.sku_id) query = query.where({ sku_id: filter.sku_id }); if (filter.platform) query = query.where({ platform: filter.platform }); if (filter.return_status) query = query.where({ return_status: filter.return_status }); if (filter.start_date) query = query.where('created_at', '>=', filter.start_date); if (filter.end_date) query = query.where('created_at', '<=', filter.end_date); const total = await query.count('id as count').first(); const records = await query.orderBy('created_at', 'desc').limit(limit).offset(offset); return { records: records as ReturnRecord[], total: total ? parseInt(total.count as string) : 0 }; } static async updateSKUReturnMetrics(tenantId: string, shopId: string, productId: string, skuId: string, platform: string, periodDays: number = 30): Promise { try { const periodEnd = new Date(); const periodStart = new Date(); periodStart.setDate(periodStart.getDate() - periodDays); const ordersCount = await db('cf_order_item') .join('cf_order', 'cf_order.id', '=', 'cf_order_item.order_id') .where('cf_order.tenant_id', tenantId) .where('cf_order.shop_id', shopId) .where('cf_order_item.product_id', productId) .where('cf_order.status', 'COMPLETED') .where('cf_order.created_at', '>=', periodStart) .count('cf_order_item.id as count') .first(); const returnsCount = await db(this.TABLE_NAME) .where({ tenant_id: tenantId, shop_id: shopId, product_id: productId, return_status: 'COMPLETED' }) .where('created_at', '>=', periodStart) .count('id as count') .first(); const refundSum = await db(this.TABLE_NAME) .where({ tenant_id: tenantId, shop_id: shopId, product_id: productId, return_status: 'COMPLETED' }) .where('created_at', '>=', periodStart) .sum('refund_amount as total') .first(); const returnReasons = await db(this.TABLE_NAME) .select('return_reason_category', db.raw('count(*) as count')) .where({ tenant_id: tenantId, shop_id: shopId, product_id: productId }) .where('created_at', '>=', periodStart) .groupBy('return_reason_category'); const byReason: Record = {}; returnReasons.forEach((r: any) => { byReason[r.return_reason_category] = parseInt(r.count); }); const totalOrders = ordersCount ? parseInt(ordersCount.count as string) : 0; const totalReturns = returnsCount ? parseInt(returnsCount.count as string) : 0; const returnRate = totalOrders > 0 ? (totalReturns / totalOrders) * 100 : 0; const totalRefund = refundSum?.total ? parseFloat(refundSum.total as string) : 0; const avgRefund = totalReturns > 0 ? totalRefund / totalReturns : 0; const threshold = await this.getThreshold(tenantId, platform, shopId); const thresholdValue = threshold?.return_rate_threshold || 30; const isHighRisk = returnRate >= thresholdValue; const id = uuidv4(); const existing = await db(this.SKU_METRICS_TABLE) .where({ tenant_id: tenantId, shop_id: shopId, product_id: productId, sku_id: skuId }) .first(); if (existing) { const oldRate = parseFloat(existing.return_rate as string) || 0; const trend = returnRate - oldRate; await db(this.SKU_METRICS_TABLE) .where({ id: existing.id }) .update({ total_orders: totalOrders, total_returns: totalReturns, return_rate: returnRate, return_rate_trend: trend, avg_refund_amount: avgRefund, total_refund_amount: totalRefund, return_by_reason: JSON.stringify(byReason), is_high_risk: isHighRisk, last_calculated_at: new Date(), calculated_period_start: periodStart, calculated_period_end: periodEnd, updated_at: new Date() }); return await db(this.SKU_METRICS_TABLE).where({ id: existing.id }).first() as SKUReturnMetrics; } else { await db(this.SKU_METRICS_TABLE).insert({ id, tenant_id: tenantId, shop_id: shopId, product_id: productId, sku_id: skuId, platform, platform_product_id: '', platform_sku_id: '', total_orders: totalOrders, total_returns: totalReturns, return_rate: returnRate, return_rate_trend: 0, avg_refund_amount: avgRefund, total_refund_amount: totalRefund, return_by_reason: JSON.stringify(byReason), return_by_month: JSON.stringify({}), high_risk_threshold: thresholdValue, is_high_risk: isHighRisk, is_auto_delisted: false, last_calculated_at: new Date(), calculated_period_start: periodStart, calculated_period_end: periodEnd }); return await db(this.SKU_METRICS_TABLE).where({ id }).first() as SKUReturnMetrics; } } catch (error) { logger.error(`[ReturnRateDatabaseService] Failed to update SKU return metrics: ${error}`); return null; } } static async getHighRiskSKUs(tenantId: string, shopId?: string, limit: number = 100): Promise { let query = db(this.SKU_METRICS_TABLE) .where({ tenant_id: tenantId, is_high_risk: true }) .orderBy('return_rate', 'desc') .limit(limit); if (shopId) query = query.where({ shop_id: shopId }); return query as SKUReturnMetrics[]; } static async getAutoDelistSKUs(tenantId: string): Promise { return db(this.SKU_METRICS_TABLE) .where({ tenant_id: tenantId, is_high_risk: true, is_auto_delisted: false }) as SKUReturnMetrics[]; } static async markSKUDelisted(id: string, delisted: boolean): Promise { const result = await db(this.SKU_METRICS_TABLE) .where({ id }) .update({ is_auto_delisted: delisted, updated_at: new Date() }); return result > 0; } static async setThreshold(threshold: Omit): Promise { const id = uuidv4(); const now = new Date(); const existing = await db(this.THRESHOLD_TABLE) .where({ tenant_id: threshold.tenant_id, threshold_type: threshold.threshold_type }) .whereNull('shop_id') .first(); if (existing) { await db(this.THRESHOLD_TABLE) .where({ id: existing.id }) .update({ ...threshold, updated_at: now }); return { ...threshold, id: existing.id, created_at: existing.created_at, updated_at: now } as ReturnRateThreshold; } await db(this.THRESHOLD_TABLE).insert({ ...threshold, id, created_at: now, updated_at: now }); return { ...threshold, id, created_at: now, updated_at: now } as ReturnRateThreshold; } static async getThreshold(tenantId: string, platform?: string, shopId?: string): Promise { let threshold = await db(this.THRESHOLD_TABLE) .where({ tenant_id: tenantId, threshold_type: 'GLOBAL' }) .first(); if (!threshold && platform) { threshold = await db(this.THRESHOLD_TABLE) .where({ tenant_id: tenantId, threshold_type: 'PLATFORM', platform }) .first(); } return threshold as ReturnRateThreshold || null; } static async getAllThresholds(tenantId: string): Promise { return db(this.THRESHOLD_TABLE).where({ tenant_id: tenantId }) as ReturnRateThreshold[]; } static async deleteThreshold(id: string): Promise { const result = await db(this.THRESHOLD_TABLE).where({ id }).del(); return result > 0; } static async getReturnRateTrend(tenantId: string, productId: string, days: number = 90): Promise<{ date: string; rate: number }[]> { const startDate = new Date(); startDate.setDate(startDate.getDate() - days); const returns = await db(this.TABLE_NAME) .select(db.raw('DATE(created_at) as date'), db.raw('count(*) as count')) .where({ tenant_id: tenantId, product_id: productId, return_status: 'COMPLETED' }) .where('created_at', '>=', startDate) .groupBy(db.raw('DATE(created_at)')) .orderBy('date'); const result: { date: string; rate: number }[] = []; for (let i = days; i >= 0; i--) { const d = new Date(); d.setDate(d.getDate() - i); const dateStr = d.toISOString().split('T')[0]; const found = returns.find((r: any) => r.date === dateStr); result.push({ date: dateStr, rate: found ? parseInt(found.count) : 0 }); } return result; } } type decimal = number;