feat: add reputation system with deals, ratings, level badges, and chat-widget deal confirmation
This commit is contained in:
208
js/services/reputation.js
Normal file
208
js/services/reputation.js
Normal file
@@ -0,0 +1,208 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user