Files
pagodera/lib/prisma/queries/listings.ts
T
Evandro Fonseca Junior d8e7d5d7e6 feat: enhance purchase intentions with chat integration and offer tracking
- Added MongoDB chat/message integration for seller responses
- Improved seller response validation and default messaging
- Integrated automatic chat creation/update when responding to intentions
- Added highest custom offer fields to listing queries and models
- Enhanced listing data with top offer information for buyers
2025-11-08 11:51:21 -03:00

622 lines
19 KiB
TypeScript

/**
* Queries de listings (anúncios) usando Prisma
* Migrado de lib/db/queries/listings.ts
*/
import { prisma } from "../app";
import { Prisma } from "@prisma/client";
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<Listing> {
// 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<Listing | null> {
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<string, string>();
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<Listing | null> {
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<boolean> {
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<void> {
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<ListingFilters, "status"> = {}
): 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<string, string>();
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,
};
});
}