import db from '../config/database'; import { logger } from '../utils/logger'; export interface ReturnRequest { orderId: string; reason: string; items: ReturnItem[]; tenantId: string; shopId: string; taskId: string; traceId: string; businessType: 'TOC' | 'TOB'; } export interface ReturnItem { productId: string; skuId: string; quantity: number; reason?: string; } export interface ReturnRecord { id: string; orderId: string; status: ReturnStatus; reason: string; items: ReturnItem[]; totalRefundAmount: number; tenantId: string; shopId: string; taskId: string; traceId: string; businessType: 'TOC' | 'TOB'; createdAt: Date; updatedAt: Date; } export type ReturnStatus = | 'PENDING' | 'APPROVED' | 'REJECTED' | 'RETURNED' | 'REFUNDED' | 'CLOSED'; export interface RefundRequest { returnId: string; approvalResult: 'APPROVED' | 'REJECTED'; approvedAmount?: number; reason?: string; tenantId: string; shopId: string; taskId: string; traceId: string; businessType: 'TOC' | 'TOB'; } export interface RefundRecord { id: string; returnId: string; orderId: string; amount: number; status: RefundStatus; method: string; tenantId: string; shopId: string; taskId: string; traceId: string; businessType: 'TOC' | 'TOB'; createdAt: Date; updatedAt: Date; } export type RefundStatus = | 'PENDING_REVIEW' | 'APPROVED' | 'PROCESSING' | 'COMPLETED' | 'FAILED' | 'CANCELLED'; export interface ServiceTicket { id: string; returnId: string; type: TicketType; priority: TicketPriority; status: TicketStatus; subject: string; description: string; messages: TicketMessage[]; tenantId: string; shopId: string; taskId: string; traceId: string; businessType: 'TOC' | 'TOB'; createdAt: Date; updatedAt: Date; } export type TicketType = | 'RETURN' | 'REFUND' | 'EXCHANGE' | 'COMPLAINT' | 'INQUIRY'; export type TicketPriority = | 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT'; export type TicketStatus = | 'OPEN' | 'IN_PROGRESS' | 'WAITING_CUSTOMER' | 'WAITING_INTERNAL' | 'RESOLVED' | 'CLOSED'; export interface TicketMessage { id: string; sender: 'CUSTOMER' | 'AGENT' | 'SYSTEM'; content: string; attachments?: string[]; createdAt: Date; } export class AfterSalesService { static async createReturnRequest(input: ReturnRequest): Promise { const { orderId, reason, items, tenantId, shopId, taskId, traceId, businessType } = input; logger.info('[AfterSalesService] Creating return request', { orderId, tenantId, shopId, taskId, traceId, businessType, itemCount: items.length, }); try { const order = await this.getOrderById(orderId, tenantId, traceId); if (!order) { throw new Error(`Order not found: ${orderId}`); } if (order.status !== 'DELIVERED' && order.status !== 'COMPLETED') { throw new Error(`Order status ${order.status} does not allow return`); } const returnDaysLimit = 30; const deliveredAt = order.deliveredAt || order.updatedAt; const daysSinceDelivery = Math.floor( (Date.now() - deliveredAt.getTime()) / (1000 * 60 * 60 * 24) ); if (daysSinceDelivery > returnDaysLimit) { throw new Error(`Return period expired (${daysSinceDelivery} days)`); } const returnId = `RET-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; const totalRefundAmount = await this.calculateRefundAmount( order, items, tenantId, traceId ); const returnRecord: ReturnRecord = { id: returnId, orderId, status: 'PENDING', reason, items, totalRefundAmount, tenantId, shopId, taskId, traceId, businessType, createdAt: new Date(), updatedAt: new Date(), }; await db('cf_return_requests').insert({ id: returnRecord.id, order_id: returnRecord.orderId, status: returnRecord.status, reason: returnRecord.reason, items: JSON.stringify(returnRecord.items), total_refund_amount: returnRecord.totalRefundAmount, tenant_id: returnRecord.tenantId, shop_id: returnRecord.shopId, task_id: returnRecord.taskId, trace_id: returnRecord.traceId, business_type: returnRecord.businessType, created_at: returnRecord.createdAt, updated_at: returnRecord.updatedAt, }); await this.createServiceTicket({ returnId, type: 'RETURN', priority: this.determinePriority(reason), subject: `Return Request - Order ${orderId}`, description: reason, tenantId, shopId, taskId, traceId, businessType, }); logger.info('[AfterSalesService] Return request created', { returnId, orderId, tenantId, traceId, status: returnRecord.status, }); return returnRecord; } catch (error: any) { logger.error('[AfterSalesService] Failed to create return request', { orderId, tenantId, traceId, error: error.message, }); throw error; } } static async processRefund(input: RefundRequest): Promise { const { returnId, approvalResult, approvedAmount, reason, tenantId, shopId, taskId, traceId, businessType } = input; logger.info('[AfterSalesService] Processing refund', { returnId, approvalResult, tenantId, shopId, taskId, traceId, businessType, }); try { const returnRecord = await this.getReturnById(returnId, tenantId, traceId); if (!returnRecord) { throw new Error(`Return request not found: ${returnId}`); } if (returnRecord.status !== 'PENDING' && returnRecord.status !== 'APPROVED') { throw new Error(`Return status ${returnRecord.status} cannot be refunded`); } const refundId = `REF-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; const refundAmount = approvedAmount || returnRecord.totalRefundAmount; let refundStatus: RefundStatus; if (approvalResult === 'REJECTED') { refundStatus = 'CANCELLED'; } else { refundStatus = 'PENDING_REVIEW'; } const refundRecord: RefundRecord = { id: refundId, returnId, orderId: returnRecord.orderId, amount: refundAmount, status: refundStatus, method: 'ORIGINAL_PAYMENT', tenantId, shopId, taskId, traceId, businessType, createdAt: new Date(), updatedAt: new Date(), }; await db('cf_refunds').insert({ id: refundRecord.id, return_id: refundRecord.returnId, order_id: refundRecord.orderId, amount: refundRecord.amount, status: refundRecord.status, method: refundRecord.method, tenant_id: refundRecord.tenantId, shop_id: refundRecord.shopId, task_id: refundRecord.taskId, trace_id: refundRecord.traceId, business_type: refundRecord.businessType, created_at: refundRecord.createdAt, updated_at: refundRecord.updatedAt, }); if (approvalResult === 'APPROVED') { await db('cf_return_requests') .where({ id: returnId, tenant_id: tenantId }) .update({ status: 'APPROVED', updated_at: new Date() }); await this.executeRefundWorkflow(refundRecord, tenantId, traceId); } else { await db('cf_return_requests') .where({ id: returnId, tenant_id: tenantId }) .update({ status: 'REJECTED', updated_at: new Date() }); } logger.info('[AfterSalesService] Refund processed', { refundId, returnId, status: refundRecord.status, tenantId, traceId, }); return refundRecord; } catch (error: any) { logger.error('[AfterSalesService] Failed to process refund', { returnId, tenantId, traceId, error: error.message, }); throw error; } } static async createServiceTicket(input: { returnId: string; type: TicketType; priority: TicketPriority; subject: string; description: string; tenantId: string; shopId: string; taskId: string; traceId: string; businessType: 'TOC' | 'TOB'; }): Promise { const { returnId, type, priority, subject, description, tenantId, shopId, taskId, traceId, businessType } = input; logger.info('[AfterSalesService] Creating service ticket', { returnId, type, priority, tenantId, shopId, taskId, traceId, businessType, }); try { const ticketId = `TKT-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; const initialMessage: TicketMessage = { id: `MSG-${Date.now()}`, sender: 'SYSTEM', content: `Ticket created: ${subject}`, createdAt: new Date(), }; const ticket: ServiceTicket = { id: ticketId, returnId, type, priority, status: 'OPEN', subject, description, messages: [initialMessage], tenantId, shopId, taskId, traceId, businessType, createdAt: new Date(), updatedAt: new Date(), }; await db('cf_service_tickets').insert({ id: ticket.id, return_id: ticket.returnId, type: ticket.type, priority: ticket.priority, status: ticket.status, subject: ticket.subject, description: ticket.description, messages: JSON.stringify(ticket.messages), tenant_id: ticket.tenantId, shop_id: ticket.shopId, task_id: ticket.taskId, trace_id: ticket.traceId, business_type: ticket.businessType, created_at: ticket.createdAt, updated_at: ticket.updatedAt, }); logger.info('[AfterSalesService] Service ticket created', { ticketId, returnId, type, priority, tenantId, traceId, }); return ticket; } catch (error: any) { logger.error('[AfterSalesService] Failed to create service ticket', { returnId, tenantId, traceId, error: error.message, }); throw error; } } static async addTicketMessage( ticketId: string, sender: 'CUSTOMER' | 'AGENT' | 'SYSTEM', content: string, options: { tenantId: string; traceId: string; attachments?: string[]; } ): Promise { const { tenantId, traceId, attachments } = options; logger.info('[AfterSalesService] Adding ticket message', { ticketId, sender, tenantId, traceId, }); try { const ticket = await db('cf_service_tickets') .where({ id: ticketId, tenant_id: tenantId }) .first(); if (!ticket) { throw new Error(`Ticket not found: ${ticketId}`); } const messages: TicketMessage[] = JSON.parse(ticket.messages || '[]'); const newMessage: TicketMessage = { id: `MSG-${Date.now()}`, sender, content, attachments, createdAt: new Date(), }; messages.push(newMessage); await db('cf_service_tickets') .where({ id: ticketId, tenant_id: tenantId }) .update({ messages: JSON.stringify(messages), status: sender === 'CUSTOMER' ? 'IN_PROGRESS' : 'WAITING_CUSTOMER', updated_at: new Date(), }); logger.info('[AfterSalesService] Ticket message added', { ticketId, sender, tenantId, traceId, }); return { ...ticket, messages, } as ServiceTicket; } catch (error: any) { logger.error('[AfterSalesService] Failed to add ticket message', { ticketId, tenantId, traceId, error: error.message, }); throw error; } } static async getReturnById(returnId: string, tenantId: string, traceId: string): Promise { const record = await db('cf_return_requests') .where({ id: returnId, tenant_id: tenantId }) .first(); if (!record) return null; return { id: record.id, orderId: record.order_id, status: record.status, reason: record.reason, items: JSON.parse(record.items || '[]'), totalRefundAmount: Number(record.total_refund_amount), tenantId: record.tenant_id, shopId: record.shop_id, taskId: record.task_id, traceId: record.trace_id, businessType: record.business_type, createdAt: record.created_at, updatedAt: record.updated_at, }; } private static async getOrderById(orderId: string, tenantId: string, traceId: string): Promise { const order = await db('cf_orders') .where({ id: orderId, tenant_id: tenantId }) .first(); return order; } private static async calculateRefundAmount( order: any, items: ReturnItem[], tenantId: string, traceId: string ): Promise { const orderItems = JSON.parse(order.items || '[]'); let totalAmount = 0; for (const returnItem of items) { const orderItem = orderItems.find((oi: any) => oi.productId === returnItem.productId && oi.skuId === returnItem.skuId ); if (orderItem) { totalAmount += orderItem.unitPrice * returnItem.quantity; } } return Number(totalAmount.toFixed(2)); } private static determinePriority(reason: string): TicketPriority { const highPriorityKeywords = ['损坏', '缺失', '错误', 'damaged', 'missing', 'wrong', 'defective']; const urgentKeywords = ['欺诈', '投诉', 'fraud', 'complaint', 'legal']; const lowerReason = reason.toLowerCase(); if (urgentKeywords.some(kw => lowerReason.includes(kw))) { return 'URGENT'; } if (highPriorityKeywords.some(kw => lowerReason.includes(kw))) { return 'HIGH'; } return 'MEDIUM'; } private static async executeRefundWorkflow( refund: RefundRecord, tenantId: string, traceId: string ): Promise { logger.info('[AfterSalesService] Executing refund workflow', { refundId: refund.id, tenantId, traceId, }); await db('cf_refunds') .where({ id: refund.id, tenant_id: tenantId }) .update({ status: 'PROCESSING', updated_at: new Date() }); await new Promise(resolve => setTimeout(resolve, 100)); await db('cf_refunds') .where({ id: refund.id, tenant_id: tenantId }) .update({ status: 'COMPLETED', updated_at: new Date() }); await db('cf_return_requests') .where({ id: refund.returnId, tenant_id: tenantId }) .update({ status: 'REFUNDED', updated_at: new Date() }); logger.info('[AfterSalesService] Refund workflow completed', { refundId: refund.id, tenantId, traceId, }); } static async initializeTables(): Promise { logger.info('[AfterSalesService] Initializing after-sales tables'); try { if (!(await db.schema.hasTable('cf_return_requests'))) { await db.schema.createTable('cf_return_requests', table => { table.string('id').primary(); table.string('order_id').notNullable(); table.enum('status', ['PENDING', 'APPROVED', 'REJECTED', 'RETURNED', 'REFUNDED', 'CLOSED']).defaultTo('PENDING'); table.text('reason'); table.json('items'); table.decimal('total_refund_amount', 12, 2).defaultTo(0); table.string('tenant_id').notNullable(); table.string('shop_id'); table.string('task_id'); table.string('trace_id'); table.enum('business_type', ['TOC', 'TOB']).defaultTo('TOC'); table.timestamp('created_at').defaultTo(db.fn.now()); table.timestamp('updated_at').defaultTo(db.fn.now()); table.index(['order_id', 'tenant_id']); table.index(['status', 'tenant_id']); }); logger.info('[AfterSalesService] Table cf_return_requests created'); } if (!(await db.schema.hasTable('cf_refunds'))) { await db.schema.createTable('cf_refunds', table => { table.string('id').primary(); table.string('return_id').notNullable(); table.string('order_id').notNullable(); table.decimal('amount', 12, 2).notNullable(); table.enum('status', ['PENDING_REVIEW', 'APPROVED', 'PROCESSING', 'COMPLETED', 'FAILED', 'CANCELLED']).defaultTo('PENDING_REVIEW'); table.string('method').defaultTo('ORIGINAL_PAYMENT'); table.string('tenant_id').notNullable(); table.string('shop_id'); table.string('task_id'); table.string('trace_id'); table.enum('business_type', ['TOC', 'TOB']).defaultTo('TOC'); table.timestamp('created_at').defaultTo(db.fn.now()); table.timestamp('updated_at').defaultTo(db.fn.now()); table.index(['return_id', 'tenant_id']); table.index(['order_id', 'tenant_id']); table.index(['status', 'tenant_id']); }); logger.info('[AfterSalesService] Table cf_refunds created'); } if (!(await db.schema.hasTable('cf_service_tickets'))) { await db.schema.createTable('cf_service_tickets', table => { table.string('id').primary(); table.string('return_id'); table.enum('type', ['RETURN', 'REFUND', 'EXCHANGE', 'COMPLAINT', 'INQUIRY']).defaultTo('INQUIRY'); table.enum('priority', ['LOW', 'MEDIUM', 'HIGH', 'URGENT']).defaultTo('MEDIUM'); table.enum('status', ['OPEN', 'IN_PROGRESS', 'WAITING_CUSTOMER', 'WAITING_INTERNAL', 'RESOLVED', 'CLOSED']).defaultTo('OPEN'); table.string('subject'); table.text('description'); table.json('messages'); table.string('tenant_id').notNullable(); table.string('shop_id'); table.string('task_id'); table.string('trace_id'); table.enum('business_type', ['TOC', 'TOB']).defaultTo('TOC'); table.timestamp('created_at').defaultTo(db.fn.now()); table.timestamp('updated_at').defaultTo(db.fn.now()); table.index(['return_id', 'tenant_id']); table.index(['status', 'tenant_id']); table.index(['priority', 'tenant_id']); }); logger.info('[AfterSalesService] Table cf_service_tickets created'); } logger.info('[AfterSalesService] After-sales tables initialized successfully'); } catch (error: any) { logger.error(`[AfterSalesService] Failed to initialize after-sales tables - error: ${error.message}`); throw error; } } }