- 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
315 lines
10 KiB
TypeScript
315 lines
10 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { secureLogger } from "@/lib/secure-logger";
|
|
import { requireAuth } from "@/lib/auth/middleware";
|
|
import { getListingById } from "@/lib/prisma/queries/listings";
|
|
import {
|
|
createPurchaseIntention,
|
|
getPurchaseIntentionsByListing,
|
|
hasPendingIntention,
|
|
getPendingIntentionByBuyerAndListing,
|
|
} from "@/lib/prisma/queries/purchase-intentions";
|
|
import { createNotification } from "@/lib/prisma/queries/notifications";
|
|
import { trackEvent } from "@/lib/prisma/queries/telemetry";
|
|
|
|
/**
|
|
* POST /api/listings/[id]/intentions
|
|
* Cria uma nova intenção de compra
|
|
*/
|
|
export async function POST(
|
|
request: NextRequest,
|
|
{ params }: { params: Promise<{ id: string }> }
|
|
) {
|
|
try {
|
|
const authResult = await requireAuth(request);
|
|
if (authResult instanceof NextResponse) return authResult;
|
|
const auth = authResult;
|
|
|
|
const { id } = await params;
|
|
const listingId = parseInt(id, 10);
|
|
if (isNaN(listingId)) {
|
|
return NextResponse.json(
|
|
{ success: false, error: "ID de anúncio inválido" },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
const body = await request.json();
|
|
const {
|
|
payment_method,
|
|
buyer_message,
|
|
custom_offer_amount,
|
|
custom_offer_currency,
|
|
} = body;
|
|
|
|
if (!payment_method || !["pix", "tibia_coins", "gold_coins", "trade"].includes(payment_method)) {
|
|
return NextResponse.json(
|
|
{ success: false, error: "Método de pagamento inválido" },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Buscar listing
|
|
const listing = await getListingById(listingId);
|
|
if (!listing) {
|
|
return NextResponse.json(
|
|
{ success: false, error: "Anúncio não encontrado" },
|
|
{ status: 404 }
|
|
);
|
|
}
|
|
|
|
// Validar se não é o vendedor
|
|
if (listing.seller_id === auth.userId) {
|
|
return NextResponse.json(
|
|
{ success: false, error: "Você não pode criar uma intenção de compra para seu próprio anúncio" },
|
|
{ status: 403 }
|
|
);
|
|
}
|
|
|
|
// Validar se está ativo
|
|
if (listing.listing_status !== "active") {
|
|
return NextResponse.json(
|
|
{ success: false, error: "Anúncio não está disponível para compra" },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Validar se é leilão (leilões não podem ter intenções, apenas lances)
|
|
if (listing.is_auction) {
|
|
return NextResponse.json(
|
|
{ success: false, error: "Este item está em leilão. Use a funcionalidade de lance." },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Validar método de pagamento disponível
|
|
const availableMethods: string[] = [];
|
|
if (listing.price_pix) availableMethods.push("pix");
|
|
if (listing.price_tibia_coins) availableMethods.push("tibia_coins");
|
|
if (listing.price_gold_coins) availableMethods.push("gold_coins");
|
|
if (listing.trade_item_id) availableMethods.push("trade");
|
|
|
|
if (!availableMethods.includes(payment_method)) {
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: `Método de pagamento "${payment_method}" não está disponível para este anúncio`,
|
|
available_methods: availableMethods,
|
|
},
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
let normalizedOfferValue: number | null = null;
|
|
let normalizedOfferCurrency: string | null = null;
|
|
|
|
if (custom_offer_amount !== undefined && custom_offer_amount !== null) {
|
|
if (!["pix", "tibia_coins", "gold_coins"].includes(payment_method)) {
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: "Ofertas personalizadas só são permitidas para métodos monetários (PIX, Tibia Coins ou Gold Coins).",
|
|
},
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
const numericOffer = Number(custom_offer_amount);
|
|
if (Number.isNaN(numericOffer) || numericOffer <= 0) {
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: "Valor da oferta personalizada deve ser numérico e maior que zero.",
|
|
},
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
normalizedOfferCurrency =
|
|
custom_offer_currency && ["pix", "tibia_coins", "gold_coins"].includes(custom_offer_currency)
|
|
? custom_offer_currency
|
|
: payment_method;
|
|
|
|
normalizedOfferValue =
|
|
normalizedOfferCurrency === "pix"
|
|
? Number(numericOffer.toFixed(2))
|
|
: Math.floor(numericOffer);
|
|
}
|
|
|
|
// Verificar se já existe uma intenção pendente
|
|
const existingIntention = await getPendingIntentionByBuyerAndListing(listingId, auth.userId);
|
|
if (existingIntention) {
|
|
// Se já existe e ainda pode trocar o método
|
|
if (existingIntention.payment_method_change_count < 1) {
|
|
// Permite atualizar o método via endpoint específico
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: "Você já possui uma intenção de compra pendente para este anúncio",
|
|
existing_intention_id: existingIntention.intention_id,
|
|
can_change_payment_method: true,
|
|
payment_method_change_count: existingIntention.payment_method_change_count,
|
|
message: "Use o endpoint PATCH /api/purchase-intentions/[intentionId]/payment-method para trocar o método de pagamento",
|
|
},
|
|
{ status: 400 }
|
|
);
|
|
} else {
|
|
// Já atingiu o limite de trocas
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: "Você já possui uma intenção de compra pendente para este anúncio e atingiu o limite de trocas de método de pagamento",
|
|
existing_intention_id: existingIntention.intention_id,
|
|
},
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
}
|
|
|
|
// Criar intenção de compra
|
|
const intention = await createPurchaseIntention({
|
|
listing_id: listingId,
|
|
buyer_id: auth.userId,
|
|
payment_method: payment_method as any,
|
|
buyer_message: buyer_message || undefined,
|
|
custom_offer_amount: normalizedOfferValue ?? undefined,
|
|
custom_offer_currency: normalizedOfferCurrency as any,
|
|
});
|
|
|
|
// Criar notificação para o vendedor
|
|
await createNotification({
|
|
user_id: listing.seller_id,
|
|
notification_type: "purchase",
|
|
title: "Nova intenção de compra!",
|
|
message:
|
|
normalizedOfferValue && normalizedOfferCurrency
|
|
? `Alguém fez uma oferta de ${normalizedOfferCurrency === "pix" ? `R$ ${normalizedOfferValue.toFixed(2)}` : `${normalizedOfferValue.toLocaleString()} ${normalizedOfferCurrency === "tibia_coins" ? "TC" : "GP"}`} pelo item "${listing.item_name}".`
|
|
: `Alguém manifestou interesse em comprar seu item "${listing.item_name}" por ${payment_method.toUpperCase()}`,
|
|
related_id: listingId,
|
|
send_whatsapp: false,
|
|
});
|
|
|
|
// Registrar telemetria
|
|
await trackEvent({
|
|
event_type: "purchase_intention_created",
|
|
ad_id: listingId,
|
|
user_id: auth.userId,
|
|
metadata: {
|
|
payment_method,
|
|
listing_price_pix: listing.price_pix ? Number(listing.price_pix) : null,
|
|
listing_price_tc: listing.price_tibia_coins || null,
|
|
listing_price_gp: listing.price_gold_coins ? Number(listing.price_gold_coins) : null,
|
|
custom_offer_amount: normalizedOfferValue,
|
|
custom_offer_currency: normalizedOfferCurrency,
|
|
},
|
|
});
|
|
|
|
secureLogger.info("✅ Intenção de compra criada com sucesso", {
|
|
userId: auth.userId,
|
|
listingId,
|
|
intentionId: intention.intention_id,
|
|
paymentMethod: payment_method,
|
|
});
|
|
|
|
return NextResponse.json(
|
|
{
|
|
success: true,
|
|
intention,
|
|
message: "Intenção de compra criada com sucesso. O vendedor será notificado.",
|
|
},
|
|
{ status: 201 }
|
|
);
|
|
} catch (error) {
|
|
secureLogger.error("❌ Erro ao criar intenção de compra", {
|
|
error: error instanceof Error ? error.message : "Erro desconhecido",
|
|
stack: error instanceof Error ? error.stack : undefined,
|
|
});
|
|
|
|
return NextResponse.json(
|
|
{ success: false, error: "Erro ao criar intenção de compra" },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/listings/[id]/intentions
|
|
* Lista intenções de compra de um anúncio (apenas vendedor)
|
|
*/
|
|
export async function GET(
|
|
request: NextRequest,
|
|
{ params }: { params: Promise<{ id: string }> }
|
|
) {
|
|
try {
|
|
const authResult = await requireAuth(request);
|
|
if (authResult instanceof NextResponse) return authResult;
|
|
const auth = authResult;
|
|
|
|
const { id } = await params;
|
|
const listingId = parseInt(id, 10);
|
|
if (isNaN(listingId)) {
|
|
return NextResponse.json(
|
|
{ success: false, error: "ID de anúncio inválido" },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Buscar listing para verificar se o usuário é o vendedor
|
|
const listing = await getListingById(listingId);
|
|
if (!listing) {
|
|
return NextResponse.json(
|
|
{ success: false, error: "Anúncio não encontrado" },
|
|
{ status: 404 }
|
|
);
|
|
}
|
|
|
|
// Apenas o vendedor pode ver as intenções
|
|
if (listing.seller_id !== auth.userId) {
|
|
return NextResponse.json(
|
|
{ success: false, error: "Você não tem permissão para ver as intenções deste anúncio" },
|
|
{ status: 403 }
|
|
);
|
|
}
|
|
|
|
// Buscar parâmetros de filtro
|
|
const { searchParams } = new URL(request.url);
|
|
const status = searchParams.get("status") as any;
|
|
const limit = parseInt(searchParams.get("limit") || "50", 10);
|
|
|
|
// Buscar intenções
|
|
const intentions = await getPurchaseIntentionsByListing(listingId, {
|
|
status,
|
|
limit,
|
|
});
|
|
|
|
// Registrar telemetria
|
|
await trackEvent({
|
|
event_type: "purchase_intention_viewed",
|
|
ad_id: listingId,
|
|
user_id: auth.userId,
|
|
metadata: {
|
|
intentions_count: intentions.length,
|
|
},
|
|
});
|
|
|
|
return NextResponse.json(
|
|
{
|
|
success: true,
|
|
intentions,
|
|
count: intentions.length,
|
|
},
|
|
{ status: 200 }
|
|
);
|
|
} catch (error) {
|
|
secureLogger.error("❌ Erro ao buscar intenções de compra", {
|
|
error: error instanceof Error ? error.message : "Erro desconhecido",
|
|
stack: error instanceof Error ? error.stack : undefined,
|
|
});
|
|
|
|
return NextResponse.json(
|
|
{ success: false, error: "Erro ao buscar intenções de compra" },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|
|
|