/** * Queries de listings (anúncios) usando Prisma * Migrado de lib/db/queries/listings.ts */ import type { Prisma } from "@prisma/client"; import { prisma } from "../app"; import { getHighestCustomOfferForListing, getHighestCustomOffersForListings, } from "./purchase-intentions"; // Re-exportar tipos para compatibilidade export type ListingStatus = | "active" | "sold" | "cancelled" | "in_escrow" | "expired"; export interface Listing { listing_id: number; seller_id: number; item_id: number; item_name: string; quantity: number; server_name: string; price_pix: number | null; price_tibia_coins: number | null; price_gold_coins: number | null; is_auction: boolean; current_bid_amount: number | null; current_bidder_id: number | null; auction_ends_at: Date | null; description: string | null; listing_status: ListingStatus; created_at: Date; updated_at: Date; sold_at: Date | null; views_count: number; is_featured?: boolean; favorites_count?: number; inquiries_count?: number; price_history?: any; expires_at?: Date | null; trade_item_id?: number | null; trade_item_name?: string | null; trade_item_quantity?: number | null; min_bid_increment?: number | null; initial_price_pix?: number | null; initial_price_tibia_coins?: number | null; initial_price_gold_coins?: number | null; seller_character_name?: string | null; highest_custom_offer_amount?: number | null; highest_custom_offer_currency?: string | null; } export interface CreateListingData { seller_id: number; item_id: number; item_name: string; quantity: number; server_name: string; price_pix?: number; price_tibia_coins?: number; price_gold_coins?: number; is_auction?: boolean; auction_ends_at?: Date; description?: string; trade_item_id?: number | null; trade_item_name?: string | null; trade_item_quantity?: number | null; min_bid_increment?: number; } export interface UpdateListingData { quantity?: number; price_pix?: number; price_tibia_coins?: number; price_gold_coins?: number; description?: string; listing_status?: ListingStatus; current_bid_amount?: number; current_bidder_id?: number; } export interface ListingFilters { status?: ListingStatus; server_name?: string; item_id?: number; seller_id?: number; is_auction?: boolean; search?: string; page?: number; pageSize?: number; sortBy?: | "created_at" | "price_pix" | "price_tibia_coins" | "price_gold_coins" | "views_count"; sortOrder?: "ASC" | "DESC"; } /** * Cria um novo anúncio */ export async function createListing(data: CreateListingData): Promise { // Calcular incremento mínimo se for leilão e não foi fornecido let minBidIncrement = data.min_bid_increment; if (data.is_auction && !minBidIncrement) { const initialPrice = data.price_pix || 0; minBidIncrement = Math.max(initialPrice * 0.05, 10.0); } const listing = await prisma.listing.create({ data: { seller_id: data.seller_id, item_id: data.item_id, item_name: data.item_name, quantity: data.quantity, server_name: data.server_name, price_pix: data.price_pix || null, price_tibia_coins: data.price_tibia_coins || null, price_gold_coins: data.price_gold_coins ? BigInt(data.price_gold_coins) : null, is_auction: data.is_auction || false, auction_ends_at: data.auction_ends_at || null, description: data.description || null, trade_item_id: data.trade_item_id || null, trade_item_name: data.trade_item_name || null, trade_item_quantity: data.trade_item_quantity || null, min_bid_increment: minBidIncrement || null, initial_price_pix: data.is_auction ? data.price_pix || null : null, initial_price_tibia_coins: data.is_auction ? data.price_tibia_coins || null : null, initial_price_gold_coins: data.is_auction && data.price_gold_coins ? BigInt(data.price_gold_coins) : null, }, }); // Buscar character name separadamente const character = await prisma.userCharacter.findFirst({ where: { user_id: data.seller_id, server_name: data.server_name, is_main: true, }, }); // Converter todos os campos Decimal e BigInt para números JavaScript return { ...listing, price_pix: listing.price_pix ? Number(listing.price_pix) : null, current_bid_amount: listing.current_bid_amount ? Number(listing.current_bid_amount) : null, min_bid_increment: listing.min_bid_increment ? Number(listing.min_bid_increment) : null, initial_price_pix: listing.initial_price_pix ? Number(listing.initial_price_pix) : null, price_gold_coins: listing.price_gold_coins ? Number(listing.price_gold_coins) : null, initial_price_gold_coins: listing.initial_price_gold_coins ? Number(listing.initial_price_gold_coins) : null, seller_character_name: character?.character_name || null, highest_custom_offer_amount: null, highest_custom_offer_currency: null, } as Listing; } /** * Busca anúncio por ID */ export async function getListingById( listingId: number, ): Promise { const listing = await prisma.listing.findUnique({ where: { listing_id: listingId }, }); if (!listing) { return null; } // Buscar character name separadamente const character = await prisma.userCharacter.findFirst({ where: { user_id: listing.seller_id, server_name: listing.server_name, is_main: true, }, }); const highestOffer = await getHighestCustomOfferForListing(listingId); // Converter todos os campos Decimal e BigInt para números JavaScript return { ...listing, price_pix: listing.price_pix ? Number(listing.price_pix) : null, current_bid_amount: listing.current_bid_amount ? Number(listing.current_bid_amount) : null, min_bid_increment: listing.min_bid_increment ? Number(listing.min_bid_increment) : null, initial_price_pix: listing.initial_price_pix ? Number(listing.initial_price_pix) : null, price_gold_coins: listing.price_gold_coins ? Number(listing.price_gold_coins) : null, initial_price_gold_coins: listing.initial_price_gold_coins ? Number(listing.initial_price_gold_coins) : null, seller_character_name: character?.character_name || null, highest_custom_offer_amount: highestOffer?.amount ?? null, highest_custom_offer_currency: highestOffer?.currency ?? null, } as Listing; } /** * Lista anúncios com filtros e paginação */ export async function getListings( filters: ListingFilters = {}, ): Promise<{ listings: Listing[]; total: number }> { const { status, server_name, item_id, seller_id, is_auction, search, page = 1, pageSize = 20, sortBy = "created_at", sortOrder = "DESC", } = filters; const where: Prisma.ListingWhereInput = {}; if (status) { where.listing_status = status; } if (server_name) { where.server_name = server_name; } if (item_id) { where.item_id = item_id; } if (seller_id) { where.seller_id = seller_id; } if (is_auction !== undefined) { where.is_auction = is_auction; } if (search) { where.OR = [ { item_name: { contains: search, mode: "insensitive" } }, { description: { contains: search, mode: "insensitive" } }, ]; } // Validar sortBy const validSortColumns = [ "created_at", "price_pix", "price_tibia_coins", "price_gold_coins", "views_count", ]; const sortColumn = validSortColumns.includes(sortBy) ? sortBy : "created_at"; const orderBy: Prisma.ListingOrderByWithRelationInput = { [sortColumn]: sortOrder.toLowerCase() as "asc" | "desc", }; // Contar total const total = await prisma.listing.count({ where }); // Buscar listings com paginação const listings = await prisma.listing.findMany({ where, orderBy, skip: (page - 1) * pageSize, take: pageSize, }); // Buscar character names para cada listing (otimizado com uma query por seller_id + server_name) const sellerServerPairs = new Set( listings.map((l) => `${l.seller_id}:${l.server_name}`), ); const charactersMap = new Map(); for (const pair of sellerServerPairs) { const [sellerId, serverName] = pair.split(":"); const character = await prisma.userCharacter.findFirst({ where: { user_id: parseInt(sellerId, 10), server_name: serverName, is_main: true, }, }); if (character) { charactersMap.set(pair, character.character_name); } } const highestOffers = await getHighestCustomOffersForListings( listings.map((listing) => listing.listing_id), ); // Converter todos os campos Decimal e BigInt para números JavaScript const listingsWithCharacterNames = listings.map((listing) => { const key = `${listing.seller_id}:${listing.server_name}`; const highestOffer = highestOffers.get(listing.listing_id); return { ...listing, price_pix: listing.price_pix ? Number(listing.price_pix) : null, current_bid_amount: listing.current_bid_amount ? Number(listing.current_bid_amount) : null, min_bid_increment: listing.min_bid_increment ? Number(listing.min_bid_increment) : null, initial_price_pix: listing.initial_price_pix ? Number(listing.initial_price_pix) : null, price_gold_coins: listing.price_gold_coins ? Number(listing.price_gold_coins) : null, initial_price_gold_coins: listing.initial_price_gold_coins ? Number(listing.initial_price_gold_coins) : null, seller_character_name: charactersMap.get(key) || null, highest_custom_offer_amount: highestOffer?.amount ?? null, highest_custom_offer_currency: highestOffer?.currency ?? null, } as Listing; }); return { listings: listingsWithCharacterNames, total, }; } /** * Atualiza anúncio */ export async function updateListing( listingId: number, data: UpdateListingData, ): Promise { const updateData: Prisma.ListingUpdateInput = {}; if (data.quantity !== undefined) updateData.quantity = data.quantity; if (data.price_pix !== undefined) updateData.price_pix = data.price_pix; if (data.price_tibia_coins !== undefined) updateData.price_tibia_coins = data.price_tibia_coins; if (data.price_gold_coins !== undefined) updateData.price_gold_coins = BigInt(data.price_gold_coins); if (data.description !== undefined) updateData.description = data.description; if (data.listing_status !== undefined) updateData.listing_status = data.listing_status; if (data.current_bid_amount !== undefined) updateData.current_bid_amount = data.current_bid_amount; if (data.current_bidder_id !== undefined) { updateData.current_bidder_id = data.current_bidder_id; } const listing = await prisma.listing.update({ where: { listing_id: listingId }, data: updateData, }); // Buscar character name const character = await prisma.userCharacter.findFirst({ where: { user_id: listing.seller_id, server_name: listing.server_name, is_main: true, }, }); const highestOffer = await getHighestCustomOfferForListing( listing.listing_id, ); // Converter todos os campos Decimal e BigInt para números JavaScript return { ...listing, price_pix: listing.price_pix ? Number(listing.price_pix) : null, current_bid_amount: listing.current_bid_amount ? Number(listing.current_bid_amount) : null, min_bid_increment: listing.min_bid_increment ? Number(listing.min_bid_increment) : null, initial_price_pix: listing.initial_price_pix ? Number(listing.initial_price_pix) : null, price_gold_coins: listing.price_gold_coins ? Number(listing.price_gold_coins) : null, initial_price_gold_coins: listing.initial_price_gold_coins ? Number(listing.initial_price_gold_coins) : null, seller_character_name: character?.character_name || null, highest_custom_offer_amount: highestOffer?.amount ?? null, highest_custom_offer_currency: highestOffer?.currency ?? null, } as Listing; } /** * Remove anúncio */ export async function deleteListing(listingId: number): Promise { try { await prisma.listing.delete({ where: { listing_id: listingId }, }); return true; } catch (_error) { return false; } } /** * Incrementa contador de visualizações */ export async function incrementViews(listingId: number): Promise { await prisma.listing.update({ where: { listing_id: listingId }, data: { views_count: { increment: 1, }, }, }); } /** * Lista anúncios de um vendedor */ export async function getListingsBySeller( sellerId: number, filters: { status?: ListingStatus; page?: number; pageSize?: number } = {}, ): Promise<{ listings: Listing[]; total: number }> { return getListings({ seller_id: sellerId, status: filters.status, page: filters.page, pageSize: filters.pageSize, }); } /** * Lista apenas anúncios ativos (incluindo vendidos nas últimas 1 hora) */ export async function getActiveListings( filters: Omit = {}, ): Promise<{ listings: Listing[]; total: number }> { const { server_name, item_id, seller_id, is_auction, search, page = 1, pageSize = 20, sortBy = "created_at", sortOrder = "DESC", } = filters; const where: Prisma.ListingWhereInput = { OR: [ // Anúncios ativos { listing_status: "active" }, // Anúncios vendidos nas últimas 1 hora { listing_status: "sold", sold_at: { gte: new Date(Date.now() - 60 * 60 * 1000), // 1 hora atrás }, }, ], }; if (server_name) { where.server_name = server_name; } if (item_id) { where.item_id = item_id; } if (seller_id) { where.seller_id = seller_id; } if (is_auction !== undefined) { where.is_auction = is_auction; } if (search) { const searchCondition = { OR: [ { item_name: { contains: search, mode: "insensitive" as Prisma.QueryMode, }, }, { description: { contains: search, mode: "insensitive" as Prisma.QueryMode, }, }, ], }; // Combinar com a condição OR existente where.AND = [ { OR: [ { listing_status: "active" }, { listing_status: "sold", sold_at: { gte: new Date(Date.now() - 60 * 60 * 1000), }, }, ], }, searchCondition, ]; delete where.OR; } // Validar sortBy const validSortColumns = [ "created_at", "price_pix", "price_tibia_coins", "price_gold_coins", "views_count", ]; const sortColumn = validSortColumns.includes(sortBy) ? sortBy : "created_at"; const orderBy: Prisma.ListingOrderByWithRelationInput = { [sortColumn]: sortOrder.toLowerCase() as "asc" | "desc", }; // Contar total const total = await prisma.listing.count({ where }); // Buscar listings com paginação const listings = await prisma.listing.findMany({ where, orderBy, skip: (page - 1) * pageSize, take: pageSize, }); // Buscar character names para cada listing const sellerServerPairs = new Set( listings.map((l) => `${l.seller_id}:${l.server_name}`), ); const charactersMap = new Map(); for (const pair of sellerServerPairs) { const [sellerId, serverName] = pair.split(":"); const character = await prisma.userCharacter.findFirst({ where: { user_id: parseInt(sellerId, 10), server_name: serverName, is_main: true, }, }); if (character) { charactersMap.set(pair, character.character_name); } } const highestOffers = await getHighestCustomOffersForListings( listings.map((listing) => listing.listing_id), ); // Converter todos os campos Decimal e BigInt para números JavaScript const listingsWithCharacterNames = listings.map((listing) => { const key = `${listing.seller_id}:${listing.server_name}`; const highestOffer = highestOffers.get(listing.listing_id); return { ...listing, price_pix: listing.price_pix ? Number(listing.price_pix) : null, current_bid_amount: listing.current_bid_amount ? Number(listing.current_bid_amount) : null, min_bid_increment: listing.min_bid_increment ? Number(listing.min_bid_increment) : null, initial_price_pix: listing.initial_price_pix ? Number(listing.initial_price_pix) : null, price_gold_coins: listing.price_gold_coins ? Number(listing.price_gold_coins) : null, initial_price_gold_coins: listing.initial_price_gold_coins ? Number(listing.initial_price_gold_coins) : null, seller_character_name: charactersMap.get(key) || null, highest_custom_offer_amount: highestOffer?.amount ?? null, highest_custom_offer_currency: highestOffer?.currency ?? null, } as Listing; }); return { listings: listingsWithCharacterNames, total, }; } /** * Coloca um lance em um leilão */ export async function placeBid( listingId: number, bidderId: number, amount: number, _paymentMethod: "pix" | "tc" | "gp", ): Promise<{ success: boolean; listing?: Listing; error?: string; previousBidderId?: number; }> { // Usar transação para garantir atomicidade return await prisma.$transaction(async (tx) => { // Buscar listing atual const listing = await tx.listing.findUnique({ where: { listing_id: listingId }, }); if (!listing) { return { success: false, error: "Anúncio não encontrado" }; } if (!listing.is_auction) { return { success: false, error: "Este anúncio não é um leilão" }; } if (listing.listing_status !== "active") { return { success: false, error: "Leilão não está ativo" }; } if (listing.seller_id === bidderId) { return { success: false, error: "Você não pode dar lance no seu próprio anúncio", }; } // Validar valor mínimo const minIncrement = listing.min_bid_increment ? Number(listing.min_bid_increment) : 10; const currentBid = listing.current_bid_amount ? Number(listing.current_bid_amount) : 0; const minBid = currentBid + minIncrement; if (amount < minBid) { return { success: false, error: `O lance deve ser de pelo menos ${minBid.toFixed(2)}`, }; } const previousBidderId = listing.current_bidder_id; // Atualizar listing const updatedListing = await tx.listing.update({ where: { listing_id: listingId }, data: { current_bid_amount: amount, current_bidder_id: bidderId, }, }); // Buscar character name const character = await tx.userCharacter.findFirst({ where: { user_id: updatedListing.seller_id, server_name: updatedListing.server_name, is_main: true, }, }); // Converter todos os campos Decimal e BigInt para números JavaScript return { success: true, listing: { ...updatedListing, price_pix: updatedListing.price_pix ? Number(updatedListing.price_pix) : null, current_bid_amount: updatedListing.current_bid_amount ? Number(updatedListing.current_bid_amount) : null, min_bid_increment: updatedListing.min_bid_increment ? Number(updatedListing.min_bid_increment) : null, initial_price_pix: updatedListing.initial_price_pix ? Number(updatedListing.initial_price_pix) : null, price_gold_coins: updatedListing.price_gold_coins ? Number(updatedListing.price_gold_coins) : null, initial_price_gold_coins: updatedListing.initial_price_gold_coins ? Number(updatedListing.initial_price_gold_coins) : null, seller_character_name: character?.character_name || null, highest_custom_offer_amount: null, highest_custom_offer_currency: null, } as Listing, previousBidderId: previousBidderId || undefined, }; }); }