Files
kashilo/js/services/reputation.js

209 lines
6.7 KiB
JavaScript

import { directus } from './directus.js'
import { cryptoService } from './crypto.js'
const CACHE_TTL = 5 * 60 * 1000
class ReputationService {
constructor() {
this.cache = new Map()
}
async getUserHash() {
await cryptoService.ready
const publicKey = cryptoService.getPublicKey()
const encoder = new TextEncoder()
const data = encoder.encode(publicKey)
const hash = await crypto.subtle.digest('SHA-256', data)
return Array.from(new Uint8Array(hash))
.map(b => b.toString(16).padStart(2, '0'))
.join('')
}
// ── Deals ──
async createDeal(conversation) {
const userHash = await this.getUserHash()
// participant_hash_1 = buyer (initiator), participant_hash_2 = seller
const sellerHash = conversation.participant_hash_2
const buyerHash = conversation.participant_hash_1
const isSeller = sellerHash === userHash
const response = await directus.post('/items/deals', {
conversation: conversation.id,
listing: conversation.listing_id,
seller_hash: sellerHash,
buyer_hash: buyerHash,
status: 'pending',
seller_confirmed: isSeller,
buyer_confirmed: !isSeller
})
return response.data || response
}
async confirmDeal(dealId) {
const userHash = await this.getUserHash()
const deal = await this.getDeal(dealId)
if (!deal) throw new Error('Deal not found')
const isSeller = deal.seller_hash === userHash
const isBuyer = deal.buyer_hash === userHash
if (!isSeller && !isBuyer) throw new Error('Not a participant of this deal')
const update = {}
if (isSeller) update.seller_confirmed = true
if (isBuyer) update.buyer_confirmed = true
const sellerConfirmed = isSeller ? true : deal.seller_confirmed
const buyerConfirmed = isBuyer ? true : deal.buyer_confirmed
if (sellerConfirmed && buyerConfirmed) {
update.status = 'confirmed'
update.date_confirmed = new Date().toISOString()
}
const response = await directus.patch(`/items/deals/${dealId}`, update)
this.clearCache()
return response.data || response
}
async getDeal(dealId) {
const response = await directus.get(`/items/deals/${dealId}`)
return response.data || response
}
async getDealsForConversation(conversationId) {
const response = await directus.get('/items/deals', {
filter: { conversation: { _eq: conversationId } },
sort: ['-date_created']
})
return response.data || []
}
async getDealsForUser(userHash) {
const response = await directus.get('/items/deals', {
filter: {
status: { _eq: 'confirmed' },
_or: [
{ seller_hash: { _eq: userHash } },
{ buyer_hash: { _eq: userHash } }
]
},
sort: ['-date_confirmed']
})
return response.data || []
}
// ── Ratings ──
async rateDeal(dealId, score) {
if (score < 1 || score > 5) throw new Error('Score must be between 1 and 5')
const deal = await this.getDeal(dealId)
if (!deal) throw new Error('Deal not found')
if (deal.status !== 'confirmed') throw new Error('Deal must be confirmed before rating')
const userHash = await this.getUserHash()
const isSeller = deal.seller_hash === userHash
const isBuyer = deal.buyer_hash === userHash
if (!isSeller && !isBuyer) throw new Error('Not a participant of this deal')
const ratedUserHash = isSeller ? deal.buyer_hash : deal.seller_hash
const existing = await directus.get('/items/ratings', {
filter: {
deal: { _eq: dealId },
rater_hash: { _eq: userHash }
},
limit: 1
})
if (existing.data?.length > 0) throw new Error('Already rated this deal')
const response = await directus.post('/items/ratings', {
deal: dealId,
rater_hash: userHash,
rated_hash: ratedUserHash,
score,
date_created: new Date().toISOString()
})
this.clearCache()
return response.data || response
}
async getRatingsForUser(userHash) {
const response = await directus.get('/items/ratings', {
filter: { rated_hash: { _eq: userHash } },
sort: ['-date_created']
})
return response.data || []
}
// ── Reputation ──
async getReputation(userHash) {
const cached = this.cache.get(userHash)
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return cached.value
}
const [deals, ratings] = await Promise.all([
this.getDealsForUser(userHash),
this.getRatingsForUser(userHash)
])
const dealsCompleted = deals.length
const avgRating = ratings.length > 0
? ratings.reduce((sum, r) => sum + r.score, 0) / ratings.length
: 0
let accountAgeDays = 0
const dates = deals.map(d => d.date_confirmed || d.date_created).filter(Boolean)
if (dates.length > 0) {
const oldest = dates
.map(d => new Date(d).getTime())
.reduce((min, t) => Math.min(min, t), Infinity)
accountAgeDays = Math.floor((Date.now() - oldest) / (1000 * 60 * 60 * 24))
}
let level = 'new'
if (dealsCompleted >= 30 && avgRating >= 4.5) {
level = 'power'
} else if (dealsCompleted >= 10 && avgRating >= 4.0) {
level = 'trusted'
} else if (dealsCompleted >= 3 && accountAgeDays >= 30) {
level = 'active'
}
const reputation = {
deals_completed: dealsCompleted,
avg_rating: Math.round(avgRating * 100) / 100,
account_age_days: accountAgeDays,
level
}
this.cache.set(userHash, { value: reputation, timestamp: Date.now() })
return reputation
}
getLevelInfo(level) {
const levels = {
new: { badge: '⚪', i18nKey: 'reputation.level.new' },
active: { badge: '🔵', i18nKey: 'reputation.level.active' },
trusted: { badge: '🟢', i18nKey: 'reputation.level.trusted' },
power: { badge: '🟣', i18nKey: 'reputation.level.power' }
}
return levels[level] || levels.new
}
clearCache() {
this.cache.clear()
}
}
export const reputationService = new ReputationService()