Files
2025-11-11 03:08:36 -03:00

339 lines
10 KiB
TypeScript

import { type NextRequest, NextResponse } from "next/server";
import { requireAuth } from "@/lib/auth/middleware";
import { getListingById } from "@/lib/prisma/queries/listings";
import { createNotification } from "@/lib/prisma/queries/notifications";
import {
createPurchaseIntention,
getPendingIntentionByBuyerAndListing,
getPurchaseIntentionsByListing,
} from "@/lib/prisma/queries/purchase-intentions";
import { trackEvent } from "@/lib/prisma/queries/telemetry";
import { secureLogger } from "@/lib/secure-logger";
/**
* 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 (Number.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 (Number.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 },
);
}
}