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
This commit is contained in:
1 parent
d27526e026
commit
d8e7d5d7e6
19 files changed
+806
-58
No files matched your search
@@ -31,6 +31,8 @@ function extractToken(request: NextRequest): string | null {
|
||||
}
|
||||
|
||||
/**
|
||||
* Converte payload de token em objeto AuthenticatedUser.
|
||||
*/
|
||||
function toAuthenticatedUser(payload: {
|
||||
userId: number;
|
||||
username: string;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
-- Migration 020: Custom offer fields for purchase intentions
|
||||
-- Criado em: 2025-02-07
|
||||
|
||||
ALTER TABLE purchase_intentions
|
||||
ADD COLUMN IF NOT EXISTS custom_offer_amount NUMERIC(12, 2),
|
||||
ADD COLUMN IF NOT EXISTS custom_offer_currency VARCHAR(20),
|
||||
ADD COLUMN IF NOT EXISTS custom_offer_created_at TIMESTAMP WITHOUT TIME ZONE;
|
||||
|
||||
-- Atualizar registros existentes para evitar valores inconsistentes
|
||||
UPDATE purchase_intentions
|
||||
SET custom_offer_amount = NULL,
|
||||
custom_offer_currency = NULL,
|
||||
custom_offer_created_at = NULL
|
||||
WHERE custom_offer_amount IS NOT NULL
|
||||
OR custom_offer_currency IS NOT NULL
|
||||
OR custom_offer_created_at IS NOT NULL;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/**
|
||||
* Script de execução de migrations
|
||||
* Executa todas as migrations SQL em ordem
|
||||
*/
|
||||
@@ -39,6 +39,7 @@ const MIGRATIONS = [
|
||||
"017_add_items_foreign_key.sql",
|
||||
"018_migrate_listings_to_unified_items.sql",
|
||||
"019_subscription_tiers.sql",
|
||||
"020_purchase_intentions_custom_offer.sql",
|
||||
];
|
||||
|
||||
async function runMigrations() {
|
||||
|
||||
@@ -43,6 +43,9 @@ export interface PurchaseIntention {
|
||||
buyer_id: number;
|
||||
payment_method: string;
|
||||
payment_method_change_count: number;
|
||||
custom_offer_amount: number | string | null;
|
||||
custom_offer_currency: string | null;
|
||||
custom_offer_created_at: Date | string | null;
|
||||
chat_id: string | null;
|
||||
status: PurchaseIntentionStatus;
|
||||
buyer_message: string | null;
|
||||
@@ -61,17 +64,12 @@ interface PurchaseIntentionsResponse {
|
||||
count: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook para buscar intenções de compra do usuário autenticado
|
||||
* Atualiza automaticamente a cada 30 segundos
|
||||
*/
|
||||
export function useMyPurchaseIntentions(status?: PurchaseIntentionStatus) {
|
||||
export function useMyPurchaseIntentions() {
|
||||
const queryParams = new URLSearchParams();
|
||||
if (status) queryParams.set("status", status);
|
||||
queryParams.set("limit", "100"); // Limite alto para mostrar todas
|
||||
queryParams.set("limit", "100");
|
||||
|
||||
const { data, isLoading, error, refetch } = useQuery<PurchaseIntentionsResponse>({
|
||||
queryKey: ["my-purchase-intentions", status],
|
||||
queryKey: ["my-purchase-intentions"],
|
||||
queryFn: async () => {
|
||||
const res = await fetch(`/api/purchase-intentions/my?${queryParams.toString()}`);
|
||||
if (!res.ok) {
|
||||
@@ -89,8 +87,16 @@ export function useMyPurchaseIntentions(status?: PurchaseIntentionStatus) {
|
||||
enabled: true, // Sempre habilitado (autenticação é verificada no backend)
|
||||
});
|
||||
|
||||
const normalizedIntentions = (data?.intentions || []).map((intention) => ({
|
||||
...intention,
|
||||
custom_offer_amount:
|
||||
intention.custom_offer_amount !== null && intention.custom_offer_amount !== undefined
|
||||
? Number(intention.custom_offer_amount)
|
||||
: null,
|
||||
}));
|
||||
|
||||
return {
|
||||
intentions: data?.intentions || [],
|
||||
intentions: normalizedIntentions,
|
||||
count: data?.count || 0,
|
||||
isLoading,
|
||||
error,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/**
|
||||
* Queries de itens usando Prisma
|
||||
* Busca itens da tabela unificada items (PostgreSQL)
|
||||
*/
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
|
||||
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";
|
||||
@@ -42,6 +46,8 @@ export interface Listing {
|
||||
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 {
|
||||
@@ -141,6 +147,8 @@ export async function createListing(
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -165,6 +173,8 @@ export async function getListingById(listingId: number): Promise<Listing | null>
|
||||
},
|
||||
});
|
||||
|
||||
const highestOffer = await getHighestCustomOfferForListing(listingId);
|
||||
|
||||
// Converter todos os campos Decimal e BigInt para números JavaScript
|
||||
return {
|
||||
...listing,
|
||||
@@ -175,6 +185,8 @@ export async function getListingById(listingId: number): Promise<Listing | 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;
|
||||
}
|
||||
|
||||
@@ -264,9 +276,14 @@ export async function getListings(
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -276,6 +293,8 @@ export async function getListings(
|
||||
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;
|
||||
});
|
||||
|
||||
@@ -319,6 +338,8 @@ export async function updateListing(
|
||||
},
|
||||
});
|
||||
|
||||
const highestOffer = await getHighestCustomOfferForListing(listing.listing_id);
|
||||
|
||||
// Converter todos os campos Decimal e BigInt para números JavaScript
|
||||
return {
|
||||
...listing,
|
||||
@@ -329,6 +350,8 @@ export async function updateListing(
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -486,9 +509,14 @@ export async function getActiveListings(
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -498,6 +526,8 @@ export async function getActiveListings(
|
||||
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;
|
||||
});
|
||||
|
||||
@@ -580,6 +610,8 @@ export async function placeBid(
|
||||
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,
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
|
||||
import { prisma } from "../app";
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
export type PurchaseIntentionStatus = "pending" | "accepted" | "rejected" | "expired" | "completed";
|
||||
export type PaymentMethod = "pix" | "tibia_coins" | "gold_coins" | "trade";
|
||||
@@ -13,6 +14,8 @@ export interface CreatePurchaseIntentionData {
|
||||
payment_method: PaymentMethod;
|
||||
buyer_message?: string;
|
||||
expires_at?: Date;
|
||||
custom_offer_amount?: number | null;
|
||||
custom_offer_currency?: PaymentMethod | null;
|
||||
}
|
||||
|
||||
export interface PurchaseIntention {
|
||||
@@ -21,6 +24,9 @@ export interface PurchaseIntention {
|
||||
buyer_id: number;
|
||||
payment_method: string;
|
||||
payment_method_change_count: number;
|
||||
custom_offer_amount: number | null;
|
||||
custom_offer_currency: string | null;
|
||||
custom_offer_created_at: Date | null;
|
||||
status: PurchaseIntentionStatus;
|
||||
buyer_message: string | null;
|
||||
seller_response: string | null;
|
||||
@@ -42,13 +48,27 @@ export interface PurchaseIntention {
|
||||
};
|
||||
}
|
||||
|
||||
function serializePurchaseIntention(intention: any): PurchaseIntention {
|
||||
if (!intention) return intention;
|
||||
|
||||
return {
|
||||
...intention,
|
||||
custom_offer_amount:
|
||||
intention.custom_offer_amount !== null && intention.custom_offer_amount !== undefined
|
||||
? Number(intention.custom_offer_amount)
|
||||
: null,
|
||||
custom_offer_currency: intention.custom_offer_currency ?? null,
|
||||
custom_offer_created_at: intention.custom_offer_created_at ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Cria uma nova intenção de compra
|
||||
*/
|
||||
export async function createPurchaseIntention(
|
||||
data: CreatePurchaseIntentionData
|
||||
): Promise<PurchaseIntention> {
|
||||
return await prisma.purchaseIntention.create({
|
||||
const result = await prisma.purchaseIntention.create({
|
||||
data: {
|
||||
listing_id: data.listing_id,
|
||||
buyer_id: data.buyer_id,
|
||||
@@ -56,6 +76,15 @@ export async function createPurchaseIntention(
|
||||
buyer_message: data.buyer_message || null,
|
||||
expires_at: data.expires_at || null,
|
||||
status: "pending",
|
||||
custom_offer_amount:
|
||||
data.custom_offer_amount !== undefined && data.custom_offer_amount !== null
|
||||
? new Prisma.Decimal(data.custom_offer_amount)
|
||||
: null,
|
||||
custom_offer_currency: data.custom_offer_currency || null,
|
||||
custom_offer_created_at:
|
||||
data.custom_offer_amount !== undefined && data.custom_offer_amount !== null
|
||||
? new Date()
|
||||
: null,
|
||||
},
|
||||
include: {
|
||||
listing: {
|
||||
@@ -73,7 +102,9 @@ export async function createPurchaseIntention(
|
||||
},
|
||||
},
|
||||
},
|
||||
}) as PurchaseIntention;
|
||||
});
|
||||
|
||||
return serializePurchaseIntention(result);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,7 +121,7 @@ export async function getPurchaseIntentionsByListing(
|
||||
where.status = status;
|
||||
}
|
||||
|
||||
return await prisma.purchaseIntention.findMany({
|
||||
const intentions = await prisma.purchaseIntention.findMany({
|
||||
where,
|
||||
include: {
|
||||
listing: {
|
||||
@@ -110,7 +141,9 @@ export async function getPurchaseIntentionsByListing(
|
||||
},
|
||||
orderBy: { created_at: "desc" },
|
||||
take: limit,
|
||||
}) as PurchaseIntention[];
|
||||
});
|
||||
|
||||
return intentions.map(serializePurchaseIntention);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -127,7 +160,7 @@ export async function getPurchaseIntentionsByBuyer(
|
||||
where.status = status;
|
||||
}
|
||||
|
||||
return await prisma.purchaseIntention.findMany({
|
||||
const intentions = await prisma.purchaseIntention.findMany({
|
||||
where,
|
||||
include: {
|
||||
listing: {
|
||||
@@ -147,7 +180,9 @@ export async function getPurchaseIntentionsByBuyer(
|
||||
},
|
||||
orderBy: { created_at: "desc" },
|
||||
take: limit,
|
||||
}) as PurchaseIntention[];
|
||||
});
|
||||
|
||||
return intentions.map(serializePurchaseIntention);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -156,7 +191,7 @@ export async function getPurchaseIntentionsByBuyer(
|
||||
export async function getPurchaseIntentionById(
|
||||
intentionId: number
|
||||
): Promise<PurchaseIntention | null> {
|
||||
return await prisma.purchaseIntention.findUnique({
|
||||
const intention = await prisma.purchaseIntention.findUnique({
|
||||
where: { intention_id: intentionId },
|
||||
include: {
|
||||
listing: {
|
||||
@@ -174,7 +209,9 @@ export async function getPurchaseIntentionById(
|
||||
},
|
||||
},
|
||||
},
|
||||
}) as PurchaseIntention | null;
|
||||
});
|
||||
|
||||
return serializePurchaseIntention(intention);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -200,7 +237,7 @@ export async function updatePurchaseIntentionStatus(
|
||||
updateData.seller_response = sellerResponse;
|
||||
}
|
||||
|
||||
return await prisma.purchaseIntention.update({
|
||||
const updated = await prisma.purchaseIntention.update({
|
||||
where: { intention_id: intentionId },
|
||||
data: updateData,
|
||||
include: {
|
||||
@@ -219,7 +256,9 @@ export async function updatePurchaseIntentionStatus(
|
||||
},
|
||||
},
|
||||
},
|
||||
}) as PurchaseIntention;
|
||||
});
|
||||
|
||||
return serializePurchaseIntention(updated);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -259,7 +298,7 @@ export async function getPendingIntentionByBuyerAndListing(
|
||||
listingId: number,
|
||||
buyerId: number
|
||||
): Promise<PurchaseIntention | null> {
|
||||
return await prisma.purchaseIntention.findFirst({
|
||||
const intention = await prisma.purchaseIntention.findFirst({
|
||||
where: {
|
||||
listing_id: listingId,
|
||||
buyer_id: buyerId,
|
||||
@@ -284,7 +323,9 @@ export async function getPendingIntentionByBuyerAndListing(
|
||||
orderBy: {
|
||||
created_at: "desc",
|
||||
},
|
||||
}) as PurchaseIntention | null;
|
||||
});
|
||||
|
||||
return serializePurchaseIntention(intention);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -314,7 +355,7 @@ export async function updatePurchaseIntentionPaymentMethod(
|
||||
}
|
||||
|
||||
// Atualizar
|
||||
return await prisma.purchaseIntention.update({
|
||||
const updated = await prisma.purchaseIntention.update({
|
||||
where: { intention_id: intentionId },
|
||||
data: {
|
||||
payment_method: newPaymentMethod,
|
||||
@@ -337,7 +378,9 @@ export async function updatePurchaseIntentionPaymentMethod(
|
||||
},
|
||||
},
|
||||
},
|
||||
}) as PurchaseIntention;
|
||||
});
|
||||
|
||||
return serializePurchaseIntention(updated);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -372,3 +415,68 @@ export async function getPendingIntentionListingIdsForBuyer(
|
||||
return Array.from(uniqueIds);
|
||||
}
|
||||
|
||||
export async function getHighestCustomOfferForListing(
|
||||
listingId: number
|
||||
): Promise<{ amount: number; currency: string } | null> {
|
||||
const offer = await prisma.purchaseIntention.findFirst({
|
||||
where: {
|
||||
listing_id: listingId,
|
||||
status: { in: ["pending", "accepted"] },
|
||||
custom_offer_amount: { not: null },
|
||||
custom_offer_currency: { not: null },
|
||||
},
|
||||
orderBy: { custom_offer_amount: "desc" },
|
||||
select: {
|
||||
custom_offer_amount: true,
|
||||
custom_offer_currency: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!offer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
amount: Number(offer.custom_offer_amount),
|
||||
currency: offer.custom_offer_currency!,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getHighestCustomOffersForListings(
|
||||
listingIds: number[]
|
||||
): Promise<Map<number, { amount: number; currency: string }>> {
|
||||
if (listingIds.length === 0) {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
const offers = await prisma.purchaseIntention.findMany({
|
||||
where: {
|
||||
listing_id: { in: listingIds },
|
||||
status: { in: ["pending", "accepted"] },
|
||||
custom_offer_amount: { not: null },
|
||||
custom_offer_currency: { not: null },
|
||||
},
|
||||
select: {
|
||||
listing_id: true,
|
||||
custom_offer_amount: true,
|
||||
custom_offer_currency: true,
|
||||
},
|
||||
orderBy: [
|
||||
{ listing_id: "asc" },
|
||||
{ custom_offer_amount: "desc" },
|
||||
],
|
||||
});
|
||||
|
||||
const result = new Map<number, { amount: number; currency: string }>();
|
||||
for (const offer of offers) {
|
||||
if (!result.has(offer.listing_id)) {
|
||||
result.set(offer.listing_id, {
|
||||
amount: Number(offer.custom_offer_amount),
|
||||
currency: offer.custom_offer_currency!,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user