Listing_SuggestPrice/backend/app/services/pricing_service.ts

120 lines
3.7 KiB
TypeScript

import env from '#start/env'
import Product from '#models/product'
import ErpService from '#services/erp_service'
import EbayService from '#services/ebay_service'
import AiService from '#services/ai_service'
import type { Suggestion } from '#services/ai_service'
import HistoryService from '#services/history_service'
import LogService from '#services/log_service'
export interface SuggestResult {
productId: number
suggestion: Suggestion
applied: boolean
oldPrice: number
newPrice: number
historyId: number
}
/**
* Orchestrator của service gợi ý giá — lõi của hệ thống.
*
* Luồng:
* 1. Lấy dữ liệu supplier (ERP) + eBay (sold/sale)
* 2. Lưu snapshot vào history
* 3. Gọi AI/rule engine -> suggestion
* 4. Lưu ai_price vào product
* 5. Hybrid áp giá: chênh lệch <= ngưỡng -> tự áp price; vượt -> chờ duyệt
*/
export default class PricingService {
/**
* Gợi ý giá cho 1 product (on-demand).
*
* @param forceApply true = luôn áp giá AI vào `price` (bỏ qua ngưỡng hybrid).
* false (mặc định) = chỉ tự áp khi chênh lệch <= ngưỡng.
*/
static async suggestForProduct(
productId: number,
username: string,
forceApply = false
): Promise<SuggestResult> {
const product = await Product.findOrFail(productId)
const [dataSources, dataEbay] = await Promise.all([
ErpService.getSupplierPricing(product),
EbayService.getMarketData(product),
])
const suggestion = await AiService.suggest({ product, dataSources, dataEbay })
const history = await HistoryService.record({
username,
productId: product.id,
dataSources,
dataEbay,
aiResult: suggestion,
})
const oldPrice = Number(product.price)
const thresholdPct = Number(env.get('PRICING_AUTO_APPLY_THRESHOLD_PCT', 5))
const diffPct = oldPrice > 0 ? (Math.abs(suggestion.suggestedPrice - oldPrice) / oldPrice) * 100 : 100
// luôn cập nhật ai_price (giá gợi ý gần nhất)
product.aiPrice = suggestion.suggestedPrice
// hybrid: chỉ tự áp khi trong ngưỡng; forceApply bỏ qua ngưỡng để luôn áp.
const applied = forceApply || diffPct <= thresholdPct
if (applied) {
product.price = suggestion.suggestedPrice
}
await product.save()
await LogService.record({
username,
actionName: applied
? forceApply
? 'Áp giá AI (force)'
: 'Tự áp giá AI'
: 'Gợi ý giá (chờ duyệt)',
action: 'suggest',
productId: product.id,
meta: { oldPrice, suggested: suggestion.suggestedPrice, diffPct: Math.round(diffPct * 100) / 100, applied },
})
return {
productId: product.id,
suggestion,
applied,
oldPrice,
newPrice: Number(product.price),
historyId: history.id,
}
}
/** Duyệt & áp giá AI gợi ý (cho trường hợp vượt ngưỡng). */
static async approve(productId: number, username: string, price?: number): Promise<Product> {
const product = await Product.findOrFail(productId)
const oldPrice = Number(product.price)
const newPrice = price ?? Number(product.aiPrice)
if (!newPrice) throw new Error('Chưa có giá AI để duyệt')
product.price = newPrice
await product.save()
await LogService.record({
username,
actionName: 'Duyệt & áp giá AI',
action: 'update',
productId: product.id,
meta: { oldPrice, newPrice },
})
return product
}
/** Lấy danh sách id product để chạy batch (mặc định toàn bộ). */
static async productIdsForBatch(): Promise<number[]> {
const rows = await Product.query().select('id')
return rows.map((r) => r.id)
}
}