style: clean up code formatting and remove trailing whitespace
refactor: reorganize imports and fix type imports
This commit is contained in:
1 parent
d3704602fd
commit
47c04de6fe
232 files changed
+7140
-4798
No files matched your search
+6
-6
@@ -102,7 +102,7 @@ class AbacatePayClient {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(
|
||||
`Failed to create billing: ${errorData.message || response.statusText}`
|
||||
`Failed to create billing: ${errorData.message || response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ class AbacatePayClient {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.config.apiKey}`,
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -124,7 +124,7 @@ class AbacatePayClient {
|
||||
throw new Error(
|
||||
`Failed to get billing status: ${
|
||||
errorData.message || response.statusText
|
||||
}`
|
||||
}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ class AbacatePayClient {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(
|
||||
`Failed to get PIX status: ${errorData.message || response.statusText}`
|
||||
`Failed to get PIX status: ${errorData.message || response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -163,7 +163,7 @@ class AbacatePayClient {
|
||||
return result.data;
|
||||
}
|
||||
|
||||
verifyWebhookSignature(payload: string, signature: string): boolean {
|
||||
verifyWebhookSignature(_payload: string, _signature: string): boolean {
|
||||
// Método mantido para compatibilidade, mas validação agora é feita via query param
|
||||
return true;
|
||||
}
|
||||
@@ -184,7 +184,7 @@ console.log(
|
||||
process.env.ABACATEPAY_WEBHOOK_SECRET
|
||||
? "✅ Configurada"
|
||||
: "❌ NÃO CONFIGURADA"
|
||||
}`
|
||||
}`,
|
||||
);
|
||||
console.log("🔧 ==========================================\n");
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { buildGeminiParts, buildItemPrompt, InlineImageData } from "@/lib/ai/item-prompt";
|
||||
import {
|
||||
buildGeminiParts,
|
||||
buildItemPrompt,
|
||||
type InlineImageData,
|
||||
} from "@/lib/ai/item-prompt";
|
||||
import { listCategoryTemplates } from "@/lib/items/sqlite-manager";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
|
||||
interface GenerateItemStructureInput {
|
||||
name: string;
|
||||
@@ -46,12 +50,14 @@ const FALLBACK_STRUCTURE = {
|
||||
};
|
||||
|
||||
export async function generateItemStructure(
|
||||
input: GenerateItemStructureInput
|
||||
input: GenerateItemStructureInput,
|
||||
): Promise<GeminiResponse> {
|
||||
const apiKey = process.env.GEMINI_API_KEY;
|
||||
|
||||
if (!apiKey) {
|
||||
secureLogger.warn("⚠️ GEMINI_API_KEY ausente – utilizando fallback estático.");
|
||||
secureLogger.warn(
|
||||
"⚠️ GEMINI_API_KEY ausente – utilizando fallback estático.",
|
||||
);
|
||||
return {
|
||||
structure: FALLBACK_STRUCTURE,
|
||||
raw: FALLBACK_STRUCTURE,
|
||||
@@ -66,7 +72,7 @@ export async function generateItemStructure(
|
||||
try {
|
||||
const templates = listCategoryTemplates();
|
||||
if (templates.length > 0) {
|
||||
const lines: string[] = ["Estruturas atuais no SQLite:"];
|
||||
const lines: string[] = ["Estruturas atuais no SQLite:"];
|
||||
templates.forEach((tpl) => {
|
||||
lines.push(`- Tabela: ${tpl.name}`);
|
||||
tpl.columns.forEach((col) => {
|
||||
@@ -75,7 +81,7 @@ export async function generateItemStructure(
|
||||
});
|
||||
sqliteSchemaText = lines.join("\n");
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (_e) {
|
||||
sqliteSchemaText = "[Falha ao listar SQLite]";
|
||||
}
|
||||
|
||||
@@ -144,7 +150,8 @@ export async function generateItemStructure(
|
||||
|
||||
if (!response.ok) {
|
||||
const status = response.status;
|
||||
const msg = bodyJson?.error?.message || `Gemini retornou status ${status}`;
|
||||
const msg =
|
||||
bodyJson?.error?.message || `Gemini retornou status ${status}`;
|
||||
const err = new Error(msg);
|
||||
(err as any).status = status;
|
||||
(err as any).response = bodyJson;
|
||||
@@ -210,7 +217,12 @@ export async function generateItemStructure(
|
||||
}
|
||||
}
|
||||
|
||||
function buildPrompt({ name, description, scope, server }: GenerateItemStructureInput) {
|
||||
function _buildPrompt({
|
||||
name,
|
||||
description,
|
||||
scope,
|
||||
server,
|
||||
}: GenerateItemStructureInput) {
|
||||
return `
|
||||
Você é um assistente que prepara carga útil em JSON para inserção de itens em banco de dados.
|
||||
|
||||
@@ -234,7 +246,9 @@ ${description}
|
||||
"""
|
||||
|
||||
Instruções:
|
||||
- Escolha uma tabela coerente com o escopo (ex.: "items_all" para global ou "items_${(server ?? "custom")
|
||||
- Escolha uma tabela coerente com o escopo (ex.: "items_all" para global ou "items_${(
|
||||
server ?? "custom"
|
||||
)
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "_")}" para servidor).
|
||||
- Dentro de "columns", inicie com "name" e "description".
|
||||
@@ -244,4 +258,3 @@ Instruções:
|
||||
- Garante que o JSON seja minuciosamente válido.
|
||||
`;
|
||||
}
|
||||
|
||||
+17
-9
@@ -1,5 +1,5 @@
|
||||
import { promises as fs } from "fs";
|
||||
import path from "path";
|
||||
import { promises as fs } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
interface PromptContext {
|
||||
name: string;
|
||||
@@ -36,7 +36,9 @@ async function loadSchemaSummary(): Promise<string> {
|
||||
const body = match[2]
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line && !line.startsWith("//") && !line.startsWith("@@"))
|
||||
.filter(
|
||||
(line) => line && !line.startsWith("//") && !line.startsWith("@@"),
|
||||
)
|
||||
.join("\n");
|
||||
|
||||
blocks.push(`Model ${modelName} {\n${body}\n}`);
|
||||
@@ -60,7 +62,10 @@ Front-end fields (AdminItemForm):
|
||||
- dynamic metadata: campos variáveis definidos por categoria (servidores, atributos, etc)
|
||||
`.trim();
|
||||
|
||||
export async function buildItemPrompt(context: PromptContext, extraInstructions?: string) {
|
||||
export async function buildItemPrompt(
|
||||
context: PromptContext,
|
||||
extraInstructions?: string,
|
||||
) {
|
||||
const schemaSummary = await loadSchemaSummary();
|
||||
|
||||
const categoriesText =
|
||||
@@ -150,10 +155,14 @@ export interface InlineImageData {
|
||||
base64Data: string;
|
||||
}
|
||||
|
||||
export async function buildGeminiParts(prompt: string, images: InlineImageData[] = []) {
|
||||
const parts: Array<{ text?: string; inline_data?: { mimeType: string; data: string } }> = [
|
||||
{ text: prompt },
|
||||
];
|
||||
export async function buildGeminiParts(
|
||||
prompt: string,
|
||||
images: InlineImageData[] = [],
|
||||
) {
|
||||
const parts: Array<{
|
||||
text?: string;
|
||||
inline_data?: { mimeType: string; data: string };
|
||||
}> = [{ text: prompt }];
|
||||
|
||||
images.slice(0, 2).forEach((image) => {
|
||||
parts.push({
|
||||
@@ -166,4 +175,3 @@ export async function buildGeminiParts(prompt: string, images: InlineImageData[]
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
@@ -44,9 +44,13 @@ export function calculateTokenCost(
|
||||
const pricing = TOKEN_PRICING[model] ?? TOKEN_PRICING["gemini-2.5-flash"];
|
||||
const inputTokens = Math.max(0, Number(usage?.promptTokenCount || 0));
|
||||
const outputTokens = Math.max(0, Number(usage?.candidatesTokenCount || 0));
|
||||
const totalTokens = Math.max(0, Number(usage?.totalTokenCount || inputTokens + outputTokens));
|
||||
const totalTokens = Math.max(
|
||||
0,
|
||||
Number(usage?.totalTokenCount || inputTokens + outputTokens),
|
||||
);
|
||||
|
||||
const inputUsd = (inputTokens / 1_000_000) * pricing.inputPerMillionTextImageVideo;
|
||||
const inputUsd =
|
||||
(inputTokens / 1_000_000) * pricing.inputPerMillionTextImageVideo;
|
||||
const outputUsd = (outputTokens / 1_000_000) * pricing.outputPerMillion;
|
||||
const totalUsd = inputUsd + outputUsd;
|
||||
|
||||
@@ -58,4 +62,4 @@ export function calculateTokenCost(
|
||||
outputUsd,
|
||||
totalUsd,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,9 @@ export interface TokenUsageRecord {
|
||||
|
||||
const KEY_PREFIX = "ai:usage";
|
||||
|
||||
export async function recordTokenUsage(record: TokenUsageRecord): Promise<void> {
|
||||
export async function recordTokenUsage(
|
||||
record: TokenUsageRecord,
|
||||
): Promise<void> {
|
||||
const redis = await getRedisClient();
|
||||
const dayKey = `${KEY_PREFIX}:day:${new Date(record.timestamp).toISOString().slice(0, 10)}`; // YYYY-MM-DD
|
||||
const listKey = `${KEY_PREFIX}:records`;
|
||||
@@ -31,4 +33,4 @@ export async function recordTokenUsage(record: TokenUsageRecord): Promise<void>
|
||||
pipeline.lTrim(listKey, 0, 9999);
|
||||
|
||||
await pipeline.exec();
|
||||
}
|
||||
}
|
||||
+5
-6
@@ -31,7 +31,7 @@ function getJWTRefreshSecret(): string {
|
||||
const secret = process.env.JWT_REFRESH_SECRET;
|
||||
if (!secret) {
|
||||
throw new Error(
|
||||
"JWT_REFRESH_SECRET não está definido nas variáveis de ambiente"
|
||||
"JWT_REFRESH_SECRET não está definido nas variáveis de ambiente",
|
||||
);
|
||||
}
|
||||
return secret;
|
||||
@@ -55,7 +55,7 @@ interface TokenOptions {
|
||||
export function generateAccessToken(
|
||||
userId: number,
|
||||
username: string,
|
||||
options?: TokenOptions
|
||||
options?: TokenOptions,
|
||||
): string {
|
||||
const payload: TokenPayload = {
|
||||
userId,
|
||||
@@ -79,7 +79,7 @@ export function generateAccessToken(
|
||||
export function generateRefreshToken(
|
||||
userId: number,
|
||||
username: string,
|
||||
options?: TokenOptions
|
||||
options?: TokenOptions,
|
||||
): string {
|
||||
const payload: TokenPayload = {
|
||||
userId,
|
||||
@@ -118,7 +118,7 @@ export function verifyAccessToken(token: string): {
|
||||
tierSlug: decoded.tierSlug,
|
||||
tierLevel: decoded.tierLevel,
|
||||
};
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -147,8 +147,7 @@ export function verifyRefreshToken(token: string): {
|
||||
tierSlug: decoded.tierSlug,
|
||||
tierLevel: decoded.tierLevel,
|
||||
};
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
+20
-14
@@ -3,10 +3,10 @@
|
||||
* Extrai e valida tokens JWT das requisições
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { cookies } from "next/headers";
|
||||
import { verifyAccessToken } from "./jwt";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { getSession } from "@/lib/redis/session";
|
||||
import { verifyAccessToken } from "./jwt";
|
||||
|
||||
export interface AuthenticatedUser {
|
||||
userId: number;
|
||||
@@ -48,8 +48,10 @@ function toAuthenticatedUser(payload: {
|
||||
}
|
||||
|
||||
export async function requireAuth(
|
||||
request: NextRequest
|
||||
): Promise<AuthenticatedUser | NextResponse<{ success: false; error: string }>> {
|
||||
request: NextRequest,
|
||||
): Promise<
|
||||
AuthenticatedUser | NextResponse<{ success: false; error: string }>
|
||||
> {
|
||||
// Tentar primeiro com Bearer token
|
||||
const token = extractToken(request);
|
||||
|
||||
@@ -77,7 +79,7 @@ export async function requireAuth(
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
// Se der erro ao acessar cookies, continuar com o erro de token
|
||||
}
|
||||
|
||||
@@ -86,7 +88,7 @@ export async function requireAuth(
|
||||
success: false,
|
||||
error: "Token de autenticação não fornecido ou inválido",
|
||||
},
|
||||
{ status: 401 }
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -98,7 +100,7 @@ export async function requireAuth(
|
||||
* @returns Dados do usuário ou null
|
||||
*/
|
||||
export async function optionalAuth(
|
||||
request: NextRequest
|
||||
request: NextRequest,
|
||||
): Promise<AuthenticatedUser | null> {
|
||||
// Tentar primeiro com Bearer token
|
||||
const token = extractToken(request);
|
||||
@@ -125,7 +127,7 @@ export async function optionalAuth(
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
// Ignorar erros de cookie
|
||||
}
|
||||
|
||||
@@ -133,8 +135,10 @@ export async function optionalAuth(
|
||||
}
|
||||
|
||||
export async function requireAdminUser(
|
||||
request: NextRequest
|
||||
): Promise<AuthenticatedUser | NextResponse<{ success: false; error: string }>> {
|
||||
request: NextRequest,
|
||||
): Promise<
|
||||
AuthenticatedUser | NextResponse<{ success: false; error: string }>
|
||||
> {
|
||||
const auth = await requireAuth(request);
|
||||
|
||||
if (auth instanceof NextResponse) {
|
||||
@@ -147,7 +151,7 @@ export async function requireAdminUser(
|
||||
success: false,
|
||||
error: "Acesso restrito ao administrador.",
|
||||
},
|
||||
{ status: 403 }
|
||||
{ status: 403 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -156,8 +160,10 @@ export async function requireAdminUser(
|
||||
|
||||
export async function requireTierLevel(
|
||||
request: NextRequest,
|
||||
minimumLevel: number
|
||||
): Promise<AuthenticatedUser | NextResponse<{ success: false; error: string }>> {
|
||||
minimumLevel: number,
|
||||
): Promise<
|
||||
AuthenticatedUser | NextResponse<{ success: false; error: string }>
|
||||
> {
|
||||
const auth = await requireAuth(request);
|
||||
|
||||
if (auth instanceof NextResponse) {
|
||||
@@ -172,7 +178,7 @@ export async function requireTierLevel(
|
||||
success: false,
|
||||
error: "Plano atual não possui acesso a este recurso.",
|
||||
},
|
||||
{ status: 403 }
|
||||
{ status: 403 },
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ export async function hashPassword(password: string): Promise<string> {
|
||||
*/
|
||||
export async function comparePassword(
|
||||
password: string,
|
||||
hash: string
|
||||
hash: string,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const match = await bcrypt.compare(password, hash);
|
||||
@@ -40,4 +40,3 @@ export async function comparePassword(
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
* Agregação de métricas e expiração de anúncios
|
||||
*/
|
||||
|
||||
import { aggregateHourlyMetrics } from "@/lib/db/queries/telemetry";
|
||||
import { query } from "@/lib/db/index";
|
||||
import { aggregateHourlyMetrics } from "@/lib/db/queries/telemetry";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
|
||||
/**
|
||||
@@ -14,9 +14,9 @@ import { secureLogger } from "@/lib/secure-logger";
|
||||
export async function aggregateTelemetry(): Promise<void> {
|
||||
try {
|
||||
secureLogger.info("🔄 Iniciando agregação de telemetria...");
|
||||
|
||||
|
||||
await aggregateHourlyMetrics();
|
||||
|
||||
|
||||
secureLogger.info("✅ Agregação de telemetria concluída");
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao agregar telemetria", error);
|
||||
@@ -45,7 +45,7 @@ export async function expireOldListings(): Promise<void> {
|
||||
)
|
||||
RETURNING listing_id
|
||||
`,
|
||||
[]
|
||||
[],
|
||||
);
|
||||
|
||||
const expiredCount = result.rowCount || 0;
|
||||
@@ -63,10 +63,10 @@ export async function expireOldListings(): Promise<void> {
|
||||
export async function updatePriceHistory(): Promise<void> {
|
||||
try {
|
||||
secureLogger.info("🔄 Verificando histórico de preços...");
|
||||
|
||||
|
||||
// O trigger já faz isso automaticamente, mas podemos adicionar lógica adicional aqui
|
||||
// Por exemplo, verificar se há inconsistências ou limpar históricos muito antigos
|
||||
|
||||
|
||||
secureLogger.info("✅ Histórico de preços verificado");
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao atualizar histórico de preços", error);
|
||||
@@ -93,4 +93,3 @@ export async function runBackgroundJobs(): Promise<void> {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
import { connectMongoDB } from "@/lib/mongodb/connection";
|
||||
import { Chat } from "@/lib/mongodb/models/Chat";
|
||||
import { Message } from "@/lib/mongodb/models/Message";
|
||||
import { getSocketIO } from "@/lib/socket/server";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { getSocketIO } from "@/lib/socket/server";
|
||||
|
||||
interface CreateSystemMessageParams {
|
||||
listingId: number;
|
||||
@@ -22,7 +22,7 @@ interface CreateSystemMessageParams {
|
||||
* Cria uma mensagem do sistema no chat e notifica os participantes
|
||||
*/
|
||||
export async function createSystemMessageInChat(
|
||||
params: CreateSystemMessageParams
|
||||
params: CreateSystemMessageParams,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await connectMongoDB();
|
||||
@@ -38,7 +38,7 @@ export async function createSystemMessageInChat(
|
||||
// Buscar dados do listing para cache
|
||||
const { getListingById } = await import("@/lib/prisma/queries/listings");
|
||||
const listing = await getListingById(params.listingId);
|
||||
|
||||
|
||||
if (!listing) {
|
||||
throw new Error("Listing não encontrado");
|
||||
}
|
||||
@@ -53,7 +53,9 @@ export async function createSystemMessageInChat(
|
||||
}
|
||||
|
||||
// Buscar personagem principal do comprador e vendedor
|
||||
const { getMainCharacter } = await import("@/lib/prisma/queries/characters");
|
||||
const { getMainCharacter } = await import(
|
||||
"@/lib/prisma/queries/characters"
|
||||
);
|
||||
const buyerCharacter = await getMainCharacter(params.buyerId);
|
||||
const sellerCharacter = await getMainCharacter(params.sellerId);
|
||||
|
||||
@@ -70,7 +72,9 @@ export async function createSystemMessageInChat(
|
||||
item_image_url: null, // Pode ser preenchido depois se necessário
|
||||
price_pix: listing.price_pix ? Number(listing.price_pix) : undefined,
|
||||
price_tibia_coins: listing.price_tibia_coins || undefined,
|
||||
price_gold_coins: listing.price_gold_coins ? Number(listing.price_gold_coins) : undefined,
|
||||
price_gold_coins: listing.price_gold_coins
|
||||
? Number(listing.price_gold_coins)
|
||||
: undefined,
|
||||
server_name: listing.server_name,
|
||||
listing_status: listing.listing_status,
|
||||
},
|
||||
@@ -136,4 +140,3 @@ export async function createSystemMessageInChat(
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Currency } from "@/types/currency";
|
||||
import type { Currency } from "@/types/currency";
|
||||
|
||||
export function formatPrice(value: number, currency: Currency = "gp"): string {
|
||||
if (value >= 1000000) {
|
||||
|
||||
+19
-15
@@ -3,7 +3,7 @@
|
||||
* Gerencia pool de conexões e fornece helpers para queries
|
||||
*/
|
||||
|
||||
import { Pool, QueryResult, QueryResultRow } from "pg";
|
||||
import { Pool, type QueryResult, type QueryResultRow } from "pg";
|
||||
|
||||
let pool: Pool | null = null;
|
||||
|
||||
@@ -13,7 +13,7 @@ let pool: Pool | null = null;
|
||||
function validateEnvVars() {
|
||||
if (!process.env.PG_DB) {
|
||||
throw new Error(
|
||||
"PG_DB não está definido. Configure a variável de ambiente PG_DB com a connection string do PostgreSQL."
|
||||
"PG_DB não está definido. Configure a variável de ambiente PG_DB com a connection string do PostgreSQL.",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@ export function getPool(): Pool {
|
||||
*/
|
||||
export async function query<T extends QueryResultRow = any>(
|
||||
text: string,
|
||||
params?: any[]
|
||||
params?: any[],
|
||||
): Promise<QueryResult<T>> {
|
||||
const poolInstance = getPool();
|
||||
const start = Date.now();
|
||||
@@ -78,15 +78,15 @@ export async function query<T extends QueryResultRow = any>(
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
const errorCode = error?.code;
|
||||
const errorStack = error instanceof Error ? error.stack : undefined;
|
||||
|
||||
|
||||
// Ignorar silenciosamente erros relacionados à tabela "items" não existir
|
||||
// Isso é esperado durante a transição e já está sendo tratado nas funções
|
||||
const isItemsTableMissing =
|
||||
errorCode === "42P01" &&
|
||||
(errorMessage.includes('relation "items" does not exist') ||
|
||||
text.includes('FROM items') ||
|
||||
text.includes('FROM "items"'));
|
||||
|
||||
const isItemsTableMissing =
|
||||
errorCode === "42P01" &&
|
||||
(errorMessage.includes('relation "items" does not exist') ||
|
||||
text.includes("FROM items") ||
|
||||
text.includes('FROM "items"'));
|
||||
|
||||
// Não logar erros esperados sobre tabela items não existir
|
||||
if (!isItemsTableMissing) {
|
||||
console.error("❌ Erro ao executar query:", {
|
||||
@@ -94,14 +94,19 @@ export async function query<T extends QueryResultRow = any>(
|
||||
error: errorMessage,
|
||||
stack: errorStack,
|
||||
});
|
||||
|
||||
|
||||
// Se o erro for sobre coluna não existir, sugerir executar migração
|
||||
if (errorMessage.includes("column") && errorMessage.includes("does not exist")) {
|
||||
console.error("\n💡 DICA: Parece que a migração do banco de dados não foi executada.");
|
||||
if (
|
||||
errorMessage.includes("column") &&
|
||||
errorMessage.includes("does not exist")
|
||||
) {
|
||||
console.error(
|
||||
"\n💡 DICA: Parece que a migração do banco de dados não foi executada.",
|
||||
);
|
||||
console.error(" Execute: pnpm run db:migrate\n");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -145,4 +150,3 @@ if (typeof process !== "undefined") {
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,17 +3,17 @@
|
||||
* Executa todas as migrations SQL em ordem
|
||||
*/
|
||||
|
||||
import { resolve } from "node:path";
|
||||
// Carregar variáveis de ambiente
|
||||
import { config } from "dotenv";
|
||||
import { resolve } from "path";
|
||||
|
||||
// Tentar carregar .env.local primeiro, depois .env
|
||||
config({ path: resolve(process.cwd(), ".env.local") });
|
||||
config({ path: resolve(process.cwd(), ".env") });
|
||||
|
||||
import { readFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { getPool, query, healthCheck } from "../index";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { getPool, healthCheck, query } from "../index";
|
||||
|
||||
const MIGRATIONS_DIR = join(__dirname, ".");
|
||||
|
||||
@@ -50,7 +50,9 @@ async function runMigrations() {
|
||||
const isHealthy = await healthCheck();
|
||||
|
||||
if (!isHealthy) {
|
||||
console.error("⌠Falha na conexão com PostgreSQL. Verifique a variável PG_DB.");
|
||||
console.error(
|
||||
"⌠Falha na conexão com PostgreSQL. Verifique a variável PG_DB.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -78,7 +80,7 @@ async function runMigrations() {
|
||||
// Verificar se já foi executada
|
||||
const checkResult = await query<{ version: string }>(
|
||||
"SELECT version FROM schema_migrations WHERE version = $1",
|
||||
[version]
|
||||
[version],
|
||||
);
|
||||
|
||||
if (checkResult.rows.length > 0) {
|
||||
@@ -95,10 +97,9 @@ async function runMigrations() {
|
||||
await query(sql);
|
||||
|
||||
// Registrar como executada
|
||||
await query(
|
||||
"INSERT INTO schema_migrations (version) VALUES ($1)",
|
||||
[version]
|
||||
);
|
||||
await query("INSERT INTO schema_migrations (version) VALUES ($1)", [
|
||||
version,
|
||||
]);
|
||||
|
||||
console.log(`✅ Migration ${version} executada com sucesso\n`);
|
||||
} catch (error) {
|
||||
@@ -108,7 +109,7 @@ async function runMigrations() {
|
||||
}
|
||||
|
||||
console.log("🎉 Todas as migrations foram executadas com sucesso!");
|
||||
|
||||
|
||||
// Fechar pool
|
||||
const pool = getPool();
|
||||
await pool.end();
|
||||
@@ -120,5 +121,3 @@ runMigrations().catch((error) => {
|
||||
console.error("⌠Erro fatal:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ export async function createBid(data: CreateBidData): Promise<Bid> {
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING *
|
||||
`,
|
||||
[data.listing_id, data.bidder_id, data.bid_amount, data.bid_type || null]
|
||||
[data.listing_id, data.bidder_id, data.bid_amount, data.bid_type || null],
|
||||
);
|
||||
|
||||
return result.rows[0];
|
||||
@@ -43,7 +43,7 @@ export async function createBid(data: CreateBidData): Promise<Bid> {
|
||||
*/
|
||||
export async function getBidsByListing(
|
||||
listingId: number,
|
||||
activeOnly: boolean = true
|
||||
activeOnly: boolean = true,
|
||||
): Promise<Bid[]> {
|
||||
let sql = `
|
||||
SELECT * FROM bids
|
||||
@@ -73,7 +73,7 @@ export async function getHighestBid(listingId: number): Promise<Bid | null> {
|
||||
ORDER BY bid_amount DESC, created_at DESC
|
||||
LIMIT 1
|
||||
`,
|
||||
[listingId]
|
||||
[listingId],
|
||||
);
|
||||
|
||||
return result.rows[0] || null;
|
||||
@@ -89,7 +89,7 @@ export async function cancelBid(bidId: number): Promise<boolean> {
|
||||
SET is_active = FALSE
|
||||
WHERE bid_id = $1
|
||||
`,
|
||||
[bidId]
|
||||
[bidId],
|
||||
);
|
||||
|
||||
return (result.rowCount ?? 0) > 0;
|
||||
@@ -103,9 +103,8 @@ export async function getBidById(bidId: number): Promise<Bid | null> {
|
||||
`
|
||||
SELECT * FROM bids WHERE bid_id = $1
|
||||
`,
|
||||
[bidId]
|
||||
[bidId],
|
||||
);
|
||||
|
||||
return result.rows[0] || null;
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export interface UpdateCharacterData {
|
||||
*/
|
||||
export async function createCharacter(
|
||||
userId: number,
|
||||
data: CreateCharacterData
|
||||
data: CreateCharacterData,
|
||||
): Promise<Character> {
|
||||
// Usar transação para garantir atomicidade
|
||||
const { getPool } = await import("../index");
|
||||
@@ -51,7 +51,7 @@ export async function createCharacter(
|
||||
SET is_main = FALSE
|
||||
WHERE user_id = $1
|
||||
`,
|
||||
[userId]
|
||||
[userId],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ export async function createCharacter(
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING *
|
||||
`,
|
||||
[userId, data.character_name, data.server_name, data.is_main || false]
|
||||
[userId, data.character_name, data.server_name, data.is_main || false],
|
||||
);
|
||||
|
||||
await client.query("COMMIT");
|
||||
@@ -86,7 +86,7 @@ export async function getUserCharacters(userId: number): Promise<Character[]> {
|
||||
WHERE user_id = $1
|
||||
ORDER BY is_main DESC, created_at ASC
|
||||
`,
|
||||
[userId]
|
||||
[userId],
|
||||
);
|
||||
|
||||
return result.rows;
|
||||
@@ -98,7 +98,7 @@ export async function getUserCharacters(userId: number): Promise<Character[]> {
|
||||
*/
|
||||
export async function getMainCharacterByUserId(
|
||||
userId: number,
|
||||
serverName: string
|
||||
serverName: string,
|
||||
): Promise<Character | null> {
|
||||
const result = await query<Character>(
|
||||
`
|
||||
@@ -107,7 +107,7 @@ export async function getMainCharacterByUserId(
|
||||
ORDER BY is_main DESC, created_at ASC
|
||||
LIMIT 1
|
||||
`,
|
||||
[userId, serverName]
|
||||
[userId, serverName],
|
||||
);
|
||||
|
||||
return result.rows[0] || null;
|
||||
@@ -117,13 +117,13 @@ export async function getMainCharacterByUserId(
|
||||
* Busca personagem por ID
|
||||
*/
|
||||
export async function getCharacterById(
|
||||
characterId: number
|
||||
characterId: number,
|
||||
): Promise<Character | null> {
|
||||
const result = await query<Character>(
|
||||
`
|
||||
SELECT * FROM user_characters WHERE character_id = $1
|
||||
`,
|
||||
[characterId]
|
||||
[characterId],
|
||||
);
|
||||
|
||||
return result.rows[0] || null;
|
||||
@@ -136,7 +136,7 @@ export async function getCharacterById(
|
||||
export async function updateCharacter(
|
||||
characterId: number,
|
||||
userId: number,
|
||||
data: UpdateCharacterData
|
||||
data: UpdateCharacterData,
|
||||
): Promise<Character> {
|
||||
// Verificar ownership primeiro
|
||||
const existing = await getCharacterById(characterId);
|
||||
@@ -164,7 +164,7 @@ export async function updateCharacter(
|
||||
SET is_main = FALSE
|
||||
WHERE user_id = $1 AND character_id != $2
|
||||
`,
|
||||
[userId, characterId]
|
||||
[userId, characterId],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -203,7 +203,7 @@ export async function updateCharacter(
|
||||
WHERE character_id = $${paramIndex}
|
||||
RETURNING *
|
||||
`,
|
||||
values
|
||||
values,
|
||||
);
|
||||
|
||||
await client.query("COMMIT");
|
||||
@@ -223,7 +223,7 @@ export async function updateCharacter(
|
||||
*/
|
||||
export async function deleteCharacter(
|
||||
characterId: number,
|
||||
userId: number
|
||||
userId: number,
|
||||
): Promise<void> {
|
||||
// Verificar ownership
|
||||
const existing = await getCharacterById(characterId);
|
||||
@@ -239,7 +239,6 @@ export async function deleteCharacter(
|
||||
`
|
||||
DELETE FROM user_characters WHERE character_id = $1
|
||||
`,
|
||||
[characterId]
|
||||
[characterId],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,9 @@ export interface UnifiedItem {
|
||||
/**
|
||||
* Busca item por ID unificado
|
||||
*/
|
||||
export async function getItemByIdUnified(itemId: number): Promise<UnifiedItem | null> {
|
||||
export async function getItemByIdUnified(
|
||||
itemId: number,
|
||||
): Promise<UnifiedItem | null> {
|
||||
try {
|
||||
const result = await query<UnifiedItem>(
|
||||
`
|
||||
@@ -49,7 +51,7 @@ export async function getItemByIdUnified(itemId: number): Promise<UnifiedItem |
|
||||
FROM items
|
||||
WHERE item_id = $1
|
||||
`,
|
||||
[itemId]
|
||||
[itemId],
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
@@ -71,7 +73,9 @@ export async function getItemByIdUnified(itemId: number): Promise<UnifiedItem |
|
||||
/**
|
||||
* Busca item por nome (case-insensitive)
|
||||
*/
|
||||
export async function getItemByNameUnified(itemName: string): Promise<UnifiedItem | null> {
|
||||
export async function getItemByNameUnified(
|
||||
itemName: string,
|
||||
): Promise<UnifiedItem | null> {
|
||||
try {
|
||||
const result = await query<UnifiedItem>(
|
||||
`
|
||||
@@ -95,7 +99,7 @@ export async function getItemByNameUnified(itemName: string): Promise<UnifiedIte
|
||||
WHERE LOWER(item_name) = LOWER($1)
|
||||
LIMIT 1
|
||||
`,
|
||||
[itemName]
|
||||
[itemName],
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
@@ -123,7 +127,7 @@ export async function itemExists(itemId: number): Promise<boolean> {
|
||||
SELECT 1 FROM items WHERE item_id = $1
|
||||
) as exists
|
||||
`,
|
||||
[itemId]
|
||||
[itemId],
|
||||
);
|
||||
|
||||
return result.rows[0]?.exists || false;
|
||||
@@ -135,4 +139,3 @@ export async function itemExists(itemId: number): Promise<boolean> {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
+115
-56
@@ -89,21 +89,26 @@ export interface ListingFilters {
|
||||
search?: string;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
sortBy?: "created_at" | "price_pix" | "price_tibia_coins" | "price_gold_coins" | "views_count";
|
||||
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> {
|
||||
export async function createListing(data: CreateListingData): Promise<Listing> {
|
||||
// Validar item_id_unified se fornecido
|
||||
if (data.item_id_unified) {
|
||||
const exists = await itemExists(data.item_id_unified);
|
||||
if (!exists) {
|
||||
throw new Error(`Item com ID unificado ${data.item_id_unified} não encontrado na tabela items`);
|
||||
throw new Error(
|
||||
`Item com ID unificado ${data.item_id_unified} não encontrado na tabela items`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,11 +167,11 @@ export async function createListing(
|
||||
data.trade_item_id || null,
|
||||
data.trade_item_name || null,
|
||||
data.trade_item_quantity || null,
|
||||
data.is_auction ? (data.price_pix || null) : null, // initial_price_pix
|
||||
data.is_auction ? (data.price_tibia_coins || null) : null, // initial_price_tibia_coins
|
||||
data.is_auction ? (data.price_gold_coins || null) : null, // initial_price_gold_coins
|
||||
data.is_auction ? data.price_pix || null : null, // initial_price_pix
|
||||
data.is_auction ? data.price_tibia_coins || null : null, // initial_price_tibia_coins
|
||||
data.is_auction ? data.price_gold_coins || null : null, // initial_price_gold_coins
|
||||
minBidIncrement || null,
|
||||
]
|
||||
],
|
||||
);
|
||||
} catch (error) {
|
||||
// Se as colunas da migração 014 ou item_id_unified não existirem, usar INSERT sem elas
|
||||
@@ -210,11 +215,14 @@ export async function createListing(
|
||||
data.trade_item_id || null,
|
||||
data.trade_item_name || null,
|
||||
data.trade_item_quantity || null,
|
||||
]
|
||||
],
|
||||
);
|
||||
} catch (error2) {
|
||||
// Se item_id_unified também não existir, usar apenas item_id legado
|
||||
if (error2 instanceof Error && error2.message.includes("does not exist")) {
|
||||
if (
|
||||
error2 instanceof Error &&
|
||||
error2.message.includes("does not exist")
|
||||
) {
|
||||
result = await query<Listing>(
|
||||
`
|
||||
INSERT INTO listings (
|
||||
@@ -251,7 +259,7 @@ export async function createListing(
|
||||
data.trade_item_id || null,
|
||||
data.trade_item_name || null,
|
||||
data.trade_item_quantity || null,
|
||||
]
|
||||
],
|
||||
);
|
||||
} else {
|
||||
throw error2;
|
||||
@@ -268,7 +276,9 @@ export async function createListing(
|
||||
/**
|
||||
* Busca anúncio por ID
|
||||
*/
|
||||
export async function getListingById(listingId: number): Promise<Listing | null> {
|
||||
export async function getListingById(
|
||||
listingId: number,
|
||||
): Promise<Listing | null> {
|
||||
let result;
|
||||
try {
|
||||
// Tentar query com todas as colunas (se migração 014 foi executada)
|
||||
@@ -298,7 +308,7 @@ export async function getListingById(listingId: number): Promise<Listing | null>
|
||||
AND uc.is_main = TRUE
|
||||
WHERE l.listing_id = $1
|
||||
`,
|
||||
[listingId]
|
||||
[listingId],
|
||||
);
|
||||
} catch (error) {
|
||||
// Se as colunas da migração 014 não existirem, usar query sem elas
|
||||
@@ -332,7 +342,7 @@ export async function getListingById(listingId: number): Promise<Listing | null>
|
||||
AND uc.is_main = TRUE
|
||||
WHERE l.listing_id = $1
|
||||
`,
|
||||
[listingId]
|
||||
[listingId],
|
||||
);
|
||||
} else {
|
||||
throw error;
|
||||
@@ -354,7 +364,7 @@ export async function getListingById(listingId: number): Promise<Listing | null>
|
||||
* Lista anúncios com filtros e paginação
|
||||
*/
|
||||
export async function getListings(
|
||||
filters: ListingFilters = {}
|
||||
filters: ListingFilters = {},
|
||||
): Promise<{ listings: Listing[]; total: number }> {
|
||||
const {
|
||||
status,
|
||||
@@ -404,36 +414,45 @@ export async function getListings(
|
||||
}
|
||||
|
||||
if (search) {
|
||||
conditions.push(`(l.item_name ILIKE $${paramIndex} OR l.description ILIKE $${paramIndex})`);
|
||||
conditions.push(
|
||||
`(l.item_name ILIKE $${paramIndex} OR l.description ILIKE $${paramIndex})`,
|
||||
);
|
||||
params.push(`%${search}%`);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "WHERE 1=1";
|
||||
const whereClause =
|
||||
conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "WHERE 1=1";
|
||||
|
||||
// Contar total
|
||||
const countResult = await query<{ count: string }>(
|
||||
`SELECT COUNT(*) as count FROM listings l ${whereClause}`,
|
||||
params
|
||||
params,
|
||||
);
|
||||
const total = parseInt(countResult.rows[0].count, 10);
|
||||
|
||||
// Validar sortBy - garantir que seja uma coluna válida
|
||||
const validSortColumns = ["created_at", "price_pix", "price_tibia_coins", "price_gold_coins", "views_count"];
|
||||
const validSortColumns = [
|
||||
"created_at",
|
||||
"price_pix",
|
||||
"price_tibia_coins",
|
||||
"price_gold_coins",
|
||||
"views_count",
|
||||
];
|
||||
const sortColumn = validSortColumns.includes(sortBy) ? sortBy : "created_at";
|
||||
const sortDirection = sortOrder === "ASC" ? "ASC" : "DESC";
|
||||
|
||||
|
||||
// Mapear colunas para usar alias 'l.' quando necessário
|
||||
const sortColumnWithAlias = `l.${sortColumn}`;
|
||||
|
||||
// Buscar listings
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
|
||||
// Incrementar paramIndex para os parâmetros de LIMIT e OFFSET
|
||||
const limitParamIndex = paramIndex;
|
||||
const offsetParamIndex = paramIndex + 1;
|
||||
const finalParams = [...params, pageSize, offset];
|
||||
|
||||
|
||||
// Tentar query com todas as colunas (se migração 014 foi executada)
|
||||
// Se falhar, usar query sem as novas colunas
|
||||
let result;
|
||||
@@ -465,10 +484,10 @@ export async function getListings(
|
||||
ORDER BY ${sortColumnWithAlias} ${sortDirection}
|
||||
LIMIT $${limitParamIndex} OFFSET $${offsetParamIndex}
|
||||
`;
|
||||
|
||||
|
||||
result = await query<Listing & { seller_character_name: string }>(
|
||||
querySql,
|
||||
finalParams
|
||||
finalParams,
|
||||
);
|
||||
} catch (error) {
|
||||
// Se as colunas da migração 014 não existirem, usar query sem elas
|
||||
@@ -503,10 +522,10 @@ export async function getListings(
|
||||
ORDER BY ${sortColumnWithAlias} ${sortDirection}
|
||||
LIMIT $${limitParamIndex} OFFSET $${offsetParamIndex}
|
||||
`;
|
||||
|
||||
|
||||
result = await query<Listing & { seller_character_name: string }>(
|
||||
fallbackQuerySql,
|
||||
finalParams
|
||||
finalParams,
|
||||
);
|
||||
} else {
|
||||
throw error;
|
||||
@@ -514,7 +533,7 @@ export async function getListings(
|
||||
}
|
||||
|
||||
// Mapear resultados para garantir que seller_character_name seja null se não existir
|
||||
const listings = result.rows.map(row => ({
|
||||
const listings = result.rows.map((row) => ({
|
||||
...row,
|
||||
seller_character_name: row.seller_character_name || null,
|
||||
}));
|
||||
@@ -530,7 +549,7 @@ export async function getListings(
|
||||
*/
|
||||
export async function updateListing(
|
||||
listingId: number,
|
||||
data: UpdateListingData
|
||||
data: UpdateListingData,
|
||||
): Promise<Listing | null> {
|
||||
const updates: string[] = [];
|
||||
const params: any[] = [];
|
||||
@@ -589,7 +608,7 @@ export async function updateListing(
|
||||
}
|
||||
|
||||
params.push(listingId);
|
||||
|
||||
|
||||
let result;
|
||||
try {
|
||||
// Tentar query com todas as colunas (se migração 014 foi executada)
|
||||
@@ -611,13 +630,15 @@ export async function updateListing(
|
||||
l.trade_item_id, l.trade_item_name, l.trade_item_quantity,
|
||||
l.min_bid_increment, l.initial_price_pix, l.initial_price_tibia_coins, l.initial_price_gold_coins
|
||||
`,
|
||||
params
|
||||
params,
|
||||
);
|
||||
|
||||
|
||||
// Buscar dados do JOIN após o UPDATE
|
||||
if (result.rows.length > 0) {
|
||||
const listingId = result.rows[0].listing_id;
|
||||
const joinResult = await query<Listing & { seller_character_name: string }>(
|
||||
const joinResult = await query<
|
||||
Listing & { seller_character_name: string }
|
||||
>(
|
||||
`
|
||||
SELECT
|
||||
l.listing_id, l.seller_id, l.item_id, l.item_name, l.quantity, l.server_name,
|
||||
@@ -643,9 +664,9 @@ export async function updateListing(
|
||||
AND uc.is_main = TRUE
|
||||
WHERE l.listing_id = $1
|
||||
`,
|
||||
[listingId]
|
||||
[listingId],
|
||||
);
|
||||
|
||||
|
||||
if (joinResult.rows.length > 0) {
|
||||
result.rows[0] = joinResult.rows[0];
|
||||
}
|
||||
@@ -674,13 +695,15 @@ export async function updateListing(
|
||||
NULL::integer as initial_price_tibia_coins,
|
||||
NULL::bigint as initial_price_gold_coins
|
||||
`,
|
||||
params
|
||||
params,
|
||||
);
|
||||
|
||||
|
||||
// Buscar dados do JOIN após o UPDATE
|
||||
if (result.rows.length > 0) {
|
||||
const listingId = result.rows[0].listing_id;
|
||||
const joinResult = await query<Listing & { seller_character_name: string }>(
|
||||
const joinResult = await query<
|
||||
Listing & { seller_character_name: string }
|
||||
>(
|
||||
`
|
||||
SELECT
|
||||
l.listing_id, l.seller_id, l.item_id, l.item_name, l.quantity, l.server_name,
|
||||
@@ -709,9 +732,9 @@ export async function updateListing(
|
||||
AND uc.is_main = TRUE
|
||||
WHERE l.listing_id = $1
|
||||
`,
|
||||
[listingId]
|
||||
[listingId],
|
||||
);
|
||||
|
||||
|
||||
if (joinResult.rows.length > 0) {
|
||||
result.rows[0] = joinResult.rows[0];
|
||||
}
|
||||
@@ -742,7 +765,7 @@ export async function deleteListing(listingId: number): Promise<boolean> {
|
||||
SET listing_status = 'cancelled'
|
||||
WHERE listing_id = $1
|
||||
`,
|
||||
[listingId]
|
||||
[listingId],
|
||||
);
|
||||
|
||||
return (result.rowCount ?? 0) > 0;
|
||||
@@ -758,7 +781,7 @@ export async function incrementViews(listingId: number): Promise<void> {
|
||||
SET views_count = views_count + 1
|
||||
WHERE listing_id = $1
|
||||
`,
|
||||
[listingId]
|
||||
[listingId],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -767,7 +790,11 @@ export async function incrementViews(listingId: number): Promise<void> {
|
||||
*/
|
||||
export async function getListingsBySeller(
|
||||
sellerId: number,
|
||||
filters: { status?: Listing["listing_status"]; page?: number; pageSize?: number } = {}
|
||||
filters: {
|
||||
status?: Listing["listing_status"];
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
} = {},
|
||||
): Promise<{ listings: Listing[]; total: number }> {
|
||||
return getListings({
|
||||
seller_id: sellerId,
|
||||
@@ -779,7 +806,7 @@ export async function getListingsBySeller(
|
||||
* Busca apenas anúncios ativos
|
||||
*/
|
||||
export async function getActiveListings(
|
||||
filters: Omit<ListingFilters, "status"> = {}
|
||||
filters: Omit<ListingFilters, "status"> = {},
|
||||
): Promise<{ listings: Listing[]; total: number }> {
|
||||
return getListings({
|
||||
...filters,
|
||||
@@ -795,8 +822,13 @@ export async function placeBid(
|
||||
listingId: number,
|
||||
bidderId: number,
|
||||
bidAmount: number,
|
||||
paymentMethod: "pix" | "tc" | "gp"
|
||||
): Promise<{ success: boolean; listing: Listing | null; previousBidderId: number | null; error?: string }> {
|
||||
_paymentMethod: "pix" | "tc" | "gp",
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
listing: Listing | null;
|
||||
previousBidderId: number | null;
|
||||
error?: string;
|
||||
}> {
|
||||
const { getPool } = await import("../index");
|
||||
const pool = getPool();
|
||||
const client = await pool.connect();
|
||||
@@ -827,12 +859,17 @@ export async function placeBid(
|
||||
LEFT JOIN items i ON i.item_id = l.item_id_unified
|
||||
WHERE l.listing_id = $1
|
||||
`,
|
||||
[listingId]
|
||||
[listingId],
|
||||
);
|
||||
|
||||
if (listingResult.rows.length === 0) {
|
||||
await client.query("ROLLBACK");
|
||||
return { success: false, listing: null, previousBidderId: null, error: "Anúncio não encontrado" };
|
||||
return {
|
||||
success: false,
|
||||
listing: null,
|
||||
previousBidderId: null,
|
||||
error: "Anúncio não encontrado",
|
||||
};
|
||||
}
|
||||
|
||||
const listing = listingResult.rows[0];
|
||||
@@ -840,22 +877,45 @@ export async function placeBid(
|
||||
// Validações
|
||||
if (listing.listing_status !== "active") {
|
||||
await client.query("ROLLBACK");
|
||||
return { success: false, listing: null, previousBidderId: null, error: "Anúncio não está ativo" };
|
||||
return {
|
||||
success: false,
|
||||
listing: null,
|
||||
previousBidderId: null,
|
||||
error: "Anúncio não está ativo",
|
||||
};
|
||||
}
|
||||
|
||||
if (!listing.is_auction) {
|
||||
await client.query("ROLLBACK");
|
||||
return { success: false, listing: null, previousBidderId: null, error: "Este anúncio não é um leilão" };
|
||||
return {
|
||||
success: false,
|
||||
listing: null,
|
||||
previousBidderId: null,
|
||||
error: "Este anúncio não é um leilão",
|
||||
};
|
||||
}
|
||||
|
||||
if (listing.seller_id === bidderId) {
|
||||
await client.query("ROLLBACK");
|
||||
return { success: false, listing: null, previousBidderId: null, error: "Você não pode dar lance no seu próprio anúncio" };
|
||||
return {
|
||||
success: false,
|
||||
listing: null,
|
||||
previousBidderId: null,
|
||||
error: "Você não pode dar lance no seu próprio anúncio",
|
||||
};
|
||||
}
|
||||
|
||||
if (listing.auction_ends_at && new Date(listing.auction_ends_at) < new Date()) {
|
||||
if (
|
||||
listing.auction_ends_at &&
|
||||
new Date(listing.auction_ends_at) < new Date()
|
||||
) {
|
||||
await client.query("ROLLBACK");
|
||||
return { success: false, listing: null, previousBidderId: null, error: "Leilão expirou" };
|
||||
return {
|
||||
success: false,
|
||||
listing: null,
|
||||
previousBidderId: null,
|
||||
error: "Leilão expirou",
|
||||
};
|
||||
}
|
||||
|
||||
// Calcular lance mínimo
|
||||
@@ -886,7 +946,7 @@ export async function placeBid(
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE listing_id = $3
|
||||
`,
|
||||
[bidAmount, bidderId, listingId]
|
||||
[bidAmount, bidderId, listingId],
|
||||
);
|
||||
|
||||
// Buscar listing atualizado com JOIN
|
||||
@@ -912,7 +972,7 @@ export async function placeBid(
|
||||
LEFT JOIN items i ON i.item_id = l.item_id_unified
|
||||
WHERE l.listing_id = $1
|
||||
`,
|
||||
[listingId]
|
||||
[listingId],
|
||||
);
|
||||
|
||||
await client.query("COMMIT");
|
||||
@@ -929,4 +989,3 @@ export async function placeBid(
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,13 @@ import { query } from "../index";
|
||||
export interface Notification {
|
||||
notification_id: number;
|
||||
user_id: number;
|
||||
notification_type: "bid" | "sale" | "purchase" | "escrow" | "dispute" | "system";
|
||||
notification_type:
|
||||
| "bid"
|
||||
| "sale"
|
||||
| "purchase"
|
||||
| "escrow"
|
||||
| "dispute"
|
||||
| "system";
|
||||
title: string;
|
||||
message: string;
|
||||
related_id: number | null;
|
||||
@@ -31,7 +37,7 @@ export interface CreateNotificationData {
|
||||
* Cria uma nova notificação
|
||||
*/
|
||||
export async function createNotification(
|
||||
data: CreateNotificationData
|
||||
data: CreateNotificationData,
|
||||
): Promise<Notification> {
|
||||
const result = await query<Notification>(
|
||||
`
|
||||
@@ -46,7 +52,7 @@ export async function createNotification(
|
||||
data.message,
|
||||
data.related_id || null,
|
||||
data.send_whatsapp || false,
|
||||
]
|
||||
],
|
||||
);
|
||||
|
||||
return result.rows[0];
|
||||
@@ -57,7 +63,7 @@ export async function createNotification(
|
||||
*/
|
||||
export async function getUserNotifications(
|
||||
userId: number,
|
||||
filters: { is_read?: boolean; limit?: number } = {}
|
||||
filters: { is_read?: boolean; limit?: number } = {},
|
||||
): Promise<Notification[]> {
|
||||
const { is_read, limit = 50 } = filters;
|
||||
|
||||
@@ -71,7 +77,8 @@ export async function getUserNotifications(
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
||||
const whereClause =
|
||||
conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
||||
|
||||
const result = await query<Notification>(
|
||||
`
|
||||
@@ -80,7 +87,7 @@ export async function getUserNotifications(
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $${paramIndex}
|
||||
`,
|
||||
[...params, limit]
|
||||
[...params, limit],
|
||||
);
|
||||
|
||||
return result.rows;
|
||||
@@ -95,7 +102,7 @@ export async function getUnreadCount(userId: number): Promise<number> {
|
||||
SELECT COUNT(*) as count FROM notifications
|
||||
WHERE user_id = $1 AND is_read = FALSE
|
||||
`,
|
||||
[userId]
|
||||
[userId],
|
||||
);
|
||||
|
||||
return parseInt(result.rows[0].count, 10);
|
||||
@@ -104,14 +111,17 @@ export async function getUnreadCount(userId: number): Promise<number> {
|
||||
/**
|
||||
* Marca notificação como lida
|
||||
*/
|
||||
export async function markAsRead(notificationId: number, userId: number): Promise<boolean> {
|
||||
export async function markAsRead(
|
||||
notificationId: number,
|
||||
userId: number,
|
||||
): Promise<boolean> {
|
||||
const result = await query(
|
||||
`
|
||||
UPDATE notifications
|
||||
SET is_read = TRUE
|
||||
WHERE notification_id = $1 AND user_id = $2
|
||||
`,
|
||||
[notificationId, userId]
|
||||
[notificationId, userId],
|
||||
);
|
||||
|
||||
return (result.rowCount ?? 0) > 0;
|
||||
@@ -127,7 +137,7 @@ export async function markAllAsRead(userId: number): Promise<number> {
|
||||
SET is_read = TRUE
|
||||
WHERE user_id = $1 AND is_read = FALSE
|
||||
`,
|
||||
[userId]
|
||||
[userId],
|
||||
);
|
||||
|
||||
return result.rowCount || 0;
|
||||
@@ -136,15 +146,17 @@ export async function markAllAsRead(userId: number): Promise<number> {
|
||||
/**
|
||||
* Remove notificação
|
||||
*/
|
||||
export async function deleteNotification(notificationId: number, userId: number): Promise<boolean> {
|
||||
export async function deleteNotification(
|
||||
notificationId: number,
|
||||
userId: number,
|
||||
): Promise<boolean> {
|
||||
const result = await query(
|
||||
`
|
||||
DELETE FROM notifications
|
||||
WHERE notification_id = $1 AND user_id = $2
|
||||
`,
|
||||
[notificationId, userId]
|
||||
[notificationId, userId],
|
||||
);
|
||||
|
||||
return (result.rowCount ?? 0) > 0;
|
||||
}
|
||||
|
||||
+17
-14
@@ -37,7 +37,7 @@ export interface ServerStats {
|
||||
*/
|
||||
export async function getAllServers(): Promise<Server[]> {
|
||||
const result = await query<Server>(
|
||||
`SELECT * FROM servers ORDER BY server_name ASC`
|
||||
`SELECT * FROM servers ORDER BY server_name ASC`,
|
||||
);
|
||||
return result.rows;
|
||||
}
|
||||
@@ -47,7 +47,7 @@ export async function getAllServers(): Promise<Server[]> {
|
||||
*/
|
||||
export async function getActiveServers(): Promise<Server[]> {
|
||||
const result = await query<Server>(
|
||||
`SELECT * FROM servers WHERE is_active = TRUE ORDER BY server_name ASC`
|
||||
`SELECT * FROM servers WHERE is_active = TRUE ORDER BY server_name ASC`,
|
||||
);
|
||||
return result.rows;
|
||||
}
|
||||
@@ -55,10 +55,12 @@ export async function getActiveServers(): Promise<Server[]> {
|
||||
/**
|
||||
* Busca servidor por nome
|
||||
*/
|
||||
export async function getServerByName(serverName: string): Promise<Server | null> {
|
||||
export async function getServerByName(
|
||||
serverName: string,
|
||||
): Promise<Server | null> {
|
||||
const result = await query<Server>(
|
||||
`SELECT * FROM servers WHERE server_name = $1`,
|
||||
[serverName]
|
||||
[serverName],
|
||||
);
|
||||
return result.rows[0] || null;
|
||||
}
|
||||
@@ -69,7 +71,7 @@ export async function getServerByName(serverName: string): Promise<Server | null
|
||||
export async function getServerById(serverId: number): Promise<Server | null> {
|
||||
const result = await query<Server>(
|
||||
`SELECT * FROM servers WHERE server_id = $1`,
|
||||
[serverId]
|
||||
[serverId],
|
||||
);
|
||||
return result.rows[0] || null;
|
||||
}
|
||||
@@ -80,9 +82,9 @@ export async function getServerById(serverId: number): Promise<Server | null> {
|
||||
export async function isServerActive(serverName: string): Promise<boolean> {
|
||||
const result = await query<{ count: string }>(
|
||||
`SELECT COUNT(*) as count FROM servers WHERE server_name = $1 AND is_active = TRUE`,
|
||||
[serverName]
|
||||
[serverName],
|
||||
);
|
||||
return parseInt(result.rows[0].count) > 0;
|
||||
return parseInt(result.rows[0].count, 10) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -96,7 +98,7 @@ export async function incrementServerStats(
|
||||
total_subscribers: number;
|
||||
total_transactions: number;
|
||||
total_volume: number;
|
||||
}>
|
||||
}>,
|
||||
): Promise<void> {
|
||||
const updates: string[] = [];
|
||||
const values: any[] = [];
|
||||
@@ -133,7 +135,7 @@ export async function incrementServerStats(
|
||||
|
||||
await query(
|
||||
`UPDATE servers SET ${updates.join(", ")} WHERE server_name = $${paramIndex}`,
|
||||
values
|
||||
values,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -142,7 +144,7 @@ export async function incrementServerStats(
|
||||
*/
|
||||
export async function updateServerStats(
|
||||
serverName: string,
|
||||
stats: ServerStats
|
||||
stats: ServerStats,
|
||||
): Promise<void> {
|
||||
const updates: string[] = [];
|
||||
const values: any[] = [];
|
||||
@@ -185,14 +187,16 @@ export async function updateServerStats(
|
||||
|
||||
await query(
|
||||
`UPDATE servers SET ${updates.join(", ")} WHERE server_name = $${paramIndex}`,
|
||||
values
|
||||
values,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Incrementa total_users de múltiplos servidores
|
||||
*/
|
||||
export async function incrementUsersForServers(serverNames: string[]): Promise<void> {
|
||||
export async function incrementUsersForServers(
|
||||
serverNames: string[],
|
||||
): Promise<void> {
|
||||
if (serverNames.length === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -202,7 +206,6 @@ export async function incrementUsersForServers(serverNames: string[]): Promise<v
|
||||
SET total_users = total_users + 1,
|
||||
last_activity_at = CURRENT_TIMESTAMP
|
||||
WHERE server_name = ANY($1::text[])`,
|
||||
[serverNames]
|
||||
[serverNames],
|
||||
);
|
||||
}
|
||||
|
||||
+45
-45
@@ -96,7 +96,7 @@ export interface MarketplaceStats {
|
||||
* Registra um evento de telemetria
|
||||
*/
|
||||
export async function trackEvent(
|
||||
data: TrackEventData
|
||||
data: TrackEventData,
|
||||
): Promise<TelemetryEvent> {
|
||||
const result = await query<TelemetryEvent>(
|
||||
`
|
||||
@@ -112,7 +112,7 @@ export async function trackEvent(
|
||||
data.user_id || null,
|
||||
data.session_id || null,
|
||||
JSON.stringify(data.metadata || {}),
|
||||
]
|
||||
],
|
||||
);
|
||||
|
||||
return result.rows[0];
|
||||
@@ -122,7 +122,7 @@ export async function trackEvent(
|
||||
* Registra múltiplos eventos em batch
|
||||
*/
|
||||
export async function trackEventsBatch(
|
||||
events: TrackEventData[]
|
||||
events: TrackEventData[],
|
||||
): Promise<TelemetryEvent[]> {
|
||||
if (events.length === 0) return [];
|
||||
|
||||
@@ -132,14 +132,14 @@ export async function trackEventsBatch(
|
||||
|
||||
for (const event of events) {
|
||||
placeholders.push(
|
||||
`($${paramIndex}, $${paramIndex + 1}, $${paramIndex + 2}, $${paramIndex + 3}, $${paramIndex + 4})`
|
||||
`($${paramIndex}, $${paramIndex + 1}, $${paramIndex + 2}, $${paramIndex + 3}, $${paramIndex + 4})`,
|
||||
);
|
||||
values.push(
|
||||
event.event_type,
|
||||
event.ad_id || null,
|
||||
event.user_id || null,
|
||||
event.session_id || null,
|
||||
JSON.stringify(event.metadata || {})
|
||||
JSON.stringify(event.metadata || {}),
|
||||
);
|
||||
paramIndex += 5;
|
||||
}
|
||||
@@ -152,7 +152,7 @@ export async function trackEventsBatch(
|
||||
VALUES ${placeholders.join(", ")}
|
||||
RETURNING *
|
||||
`,
|
||||
values
|
||||
values,
|
||||
);
|
||||
|
||||
return result.rows;
|
||||
@@ -161,13 +161,15 @@ export async function trackEventsBatch(
|
||||
/**
|
||||
* Busca analytics de um anúncio específico
|
||||
*/
|
||||
export async function getAdAnalytics(adId: number): Promise<AdAnalytics | null> {
|
||||
export async function getAdAnalytics(
|
||||
adId: number,
|
||||
): Promise<AdAnalytics | null> {
|
||||
const result = await query<AdAnalytics>(
|
||||
`
|
||||
SELECT * FROM ad_analytics
|
||||
WHERE ad_id = $1
|
||||
`,
|
||||
[adId]
|
||||
[adId],
|
||||
);
|
||||
|
||||
return result.rows[0] || null;
|
||||
@@ -179,21 +181,20 @@ export async function getAdAnalytics(adId: number): Promise<AdAnalytics | null>
|
||||
export async function getUserBehavior(
|
||||
userId: number,
|
||||
startDate?: Date,
|
||||
endDate?: Date
|
||||
endDate?: Date,
|
||||
): Promise<UserBehavior | null> {
|
||||
// Verificar se usuário existe
|
||||
const userCheck = await query<{ user_id: number }>(
|
||||
"SELECT user_id FROM users WHERE user_id = $1",
|
||||
[userId]
|
||||
[userId],
|
||||
);
|
||||
|
||||
if (userCheck.rows.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const dateFilter = startDate && endDate
|
||||
? "AND timestamp BETWEEN $2 AND $3"
|
||||
: "";
|
||||
const dateFilter =
|
||||
startDate && endDate ? "AND timestamp BETWEEN $2 AND $3" : "";
|
||||
|
||||
const params: any[] = [userId];
|
||||
if (startDate && endDate) {
|
||||
@@ -207,7 +208,7 @@ export async function getUserBehavior(
|
||||
FROM telemetry_events
|
||||
WHERE user_id = $1 AND event_type = 'search' ${dateFilter}
|
||||
`,
|
||||
params
|
||||
params,
|
||||
);
|
||||
|
||||
// Buscar filtros aplicados
|
||||
@@ -217,7 +218,7 @@ export async function getUserBehavior(
|
||||
FROM telemetry_events
|
||||
WHERE user_id = $1 AND event_type = 'filter' ${dateFilter}
|
||||
`,
|
||||
params
|
||||
params,
|
||||
);
|
||||
|
||||
// Buscar itens visualizados
|
||||
@@ -232,7 +233,7 @@ export async function getUserBehavior(
|
||||
WHERE user_id = $1 AND event_type = 'ad_view' AND ad_id IS NOT NULL ${dateFilter}
|
||||
ORDER BY timestamp DESC
|
||||
`,
|
||||
params
|
||||
params,
|
||||
);
|
||||
|
||||
// Buscar favoritos
|
||||
@@ -242,7 +243,7 @@ export async function getUserBehavior(
|
||||
FROM telemetry_events
|
||||
WHERE user_id = $1 AND event_type = 'favorite' AND ad_id IS NOT NULL ${dateFilter}
|
||||
`,
|
||||
params
|
||||
params,
|
||||
);
|
||||
|
||||
// Buscar compras
|
||||
@@ -257,7 +258,7 @@ export async function getUserBehavior(
|
||||
WHERE user_id = $1 AND event_type = 'purchase' AND ad_id IS NOT NULL ${dateFilter}
|
||||
ORDER BY timestamp DESC
|
||||
`,
|
||||
params
|
||||
params,
|
||||
);
|
||||
|
||||
// Buscar horas ativas
|
||||
@@ -272,18 +273,18 @@ export async function getUserBehavior(
|
||||
GROUP BY hour
|
||||
ORDER BY hour
|
||||
`,
|
||||
params
|
||||
params,
|
||||
);
|
||||
|
||||
// Buscar item_id dos anúncios visualizados
|
||||
const itemIds = await query<{ item_id: number }>(
|
||||
const _itemIds = await query<{ item_id: number }>(
|
||||
`
|
||||
SELECT DISTINCT l.item_id
|
||||
FROM telemetry_events te
|
||||
JOIN listings l ON l.listing_id = te.ad_id
|
||||
WHERE te.user_id = $1 AND te.event_type = 'ad_view' AND te.ad_id IS NOT NULL ${dateFilter}
|
||||
`,
|
||||
params
|
||||
params,
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -317,11 +318,10 @@ export async function getUserBehavior(
|
||||
*/
|
||||
export async function getMarketplaceStats(
|
||||
startDate?: Date,
|
||||
endDate?: Date
|
||||
endDate?: Date,
|
||||
): Promise<MarketplaceStats> {
|
||||
const dateFilter = startDate && endDate
|
||||
? "WHERE timestamp BETWEEN $1 AND $2"
|
||||
: "";
|
||||
const dateFilter =
|
||||
startDate && endDate ? "WHERE timestamp BETWEEN $1 AND $2" : "";
|
||||
const params = startDate && endDate ? [startDate, endDate] : [];
|
||||
|
||||
// Total de transações
|
||||
@@ -331,7 +331,7 @@ export async function getMarketplaceStats(
|
||||
FROM telemetry_events
|
||||
WHERE event_type = 'purchase' ${dateFilter.replace("timestamp", "timestamp")}
|
||||
`,
|
||||
params
|
||||
params,
|
||||
);
|
||||
|
||||
// Preço médio por categoria (requer join com listings)
|
||||
@@ -349,7 +349,7 @@ export async function getMarketplaceStats(
|
||||
GROUP BY l.item_name
|
||||
LIMIT 10
|
||||
`,
|
||||
params
|
||||
params,
|
||||
);
|
||||
|
||||
// Taxa de conversão
|
||||
@@ -359,7 +359,7 @@ export async function getMarketplaceStats(
|
||||
FROM telemetry_events
|
||||
WHERE event_type = 'ad_view' ${dateFilter}
|
||||
`,
|
||||
params
|
||||
params,
|
||||
);
|
||||
|
||||
const purchases = await query<{ count: string }>(
|
||||
@@ -368,12 +368,13 @@ export async function getMarketplaceStats(
|
||||
FROM telemetry_events
|
||||
WHERE event_type = 'purchase' ${dateFilter}
|
||||
`,
|
||||
params
|
||||
params,
|
||||
);
|
||||
|
||||
const viewsCount = parseInt(views.rows[0]?.count || "0", 10);
|
||||
const purchasesCount = parseInt(purchases.rows[0]?.count || "0", 10);
|
||||
const conversionRate = viewsCount > 0 ? (purchasesCount / viewsCount) * 100 : 0;
|
||||
const conversionRate =
|
||||
viewsCount > 0 ? (purchasesCount / viewsCount) * 100 : 0;
|
||||
|
||||
// Itens mais procurados
|
||||
const mostSearched = await query<{
|
||||
@@ -390,7 +391,7 @@ export async function getMarketplaceStats(
|
||||
ORDER BY COUNT(*) DESC
|
||||
LIMIT 10
|
||||
`,
|
||||
params
|
||||
params,
|
||||
);
|
||||
|
||||
// Itens mais vendidos
|
||||
@@ -411,7 +412,7 @@ export async function getMarketplaceStats(
|
||||
ORDER BY COUNT(*) DESC
|
||||
LIMIT 10
|
||||
`,
|
||||
params
|
||||
params,
|
||||
);
|
||||
|
||||
// Horários de pico
|
||||
@@ -429,7 +430,7 @@ export async function getMarketplaceStats(
|
||||
ORDER BY COUNT(*) DESC
|
||||
LIMIT 10
|
||||
`,
|
||||
params
|
||||
params,
|
||||
);
|
||||
|
||||
// Tempo médio para venda
|
||||
@@ -441,7 +442,7 @@ export async function getMarketplaceStats(
|
||||
FROM ad_analytics aa
|
||||
WHERE aa.time_to_sell IS NOT NULL
|
||||
`,
|
||||
[]
|
||||
[],
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -451,7 +452,7 @@ export async function getMarketplaceStats(
|
||||
acc[r.category] = parseFloat(r.avg_price);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, number>
|
||||
{} as Record<string, number>,
|
||||
),
|
||||
conversion_rate: conversionRate,
|
||||
most_searched_items: mostSearched.rows.map((r) => ({
|
||||
@@ -493,7 +494,7 @@ export async function aggregateHourlyMetrics(): Promise<void> {
|
||||
ON CONFLICT (ad_id) DO UPDATE SET
|
||||
impressions = ad_analytics.impressions + EXCLUDED.impressions,
|
||||
last_updated = CURRENT_TIMESTAMP
|
||||
`
|
||||
`,
|
||||
);
|
||||
|
||||
// Agregar cliques
|
||||
@@ -511,7 +512,7 @@ export async function aggregateHourlyMetrics(): Promise<void> {
|
||||
GROUP BY ad_id
|
||||
) sub
|
||||
WHERE aa.ad_id = sub.ad_id
|
||||
`
|
||||
`,
|
||||
);
|
||||
|
||||
// Atualizar CTR (Click-Through Rate)
|
||||
@@ -524,7 +525,7 @@ export async function aggregateHourlyMetrics(): Promise<void> {
|
||||
END,
|
||||
last_updated = CURRENT_TIMESTAMP
|
||||
WHERE impressions > 0
|
||||
`
|
||||
`,
|
||||
);
|
||||
|
||||
// Agregar duração média de visualização
|
||||
@@ -550,7 +551,7 @@ export async function aggregateHourlyMetrics(): Promise<void> {
|
||||
AND ad_id = aa.ad_id
|
||||
AND timestamp >= NOW() - INTERVAL '1 hour'
|
||||
)
|
||||
`
|
||||
`,
|
||||
);
|
||||
|
||||
// Calcular bounce rate (views com duração < 3 segundos)
|
||||
@@ -572,7 +573,7 @@ export async function aggregateHourlyMetrics(): Promise<void> {
|
||||
END,
|
||||
last_updated = CURRENT_TIMESTAMP
|
||||
WHERE aa.impressions > 0
|
||||
`
|
||||
`,
|
||||
);
|
||||
|
||||
// Agregar inquiries
|
||||
@@ -590,7 +591,7 @@ export async function aggregateHourlyMetrics(): Promise<void> {
|
||||
GROUP BY ad_id
|
||||
) sub
|
||||
WHERE aa.ad_id = sub.ad_id
|
||||
`
|
||||
`,
|
||||
);
|
||||
|
||||
// Agregar favoritos
|
||||
@@ -608,7 +609,7 @@ export async function aggregateHourlyMetrics(): Promise<void> {
|
||||
GROUP BY ad_id
|
||||
) sub
|
||||
WHERE aa.ad_id = sub.ad_id
|
||||
`
|
||||
`,
|
||||
);
|
||||
|
||||
// Calcular taxa de conversão
|
||||
@@ -634,7 +635,7 @@ export async function aggregateHourlyMetrics(): Promise<void> {
|
||||
AND ad_id = aa.ad_id
|
||||
AND timestamp >= NOW() - INTERVAL '1 hour'
|
||||
)
|
||||
`
|
||||
`,
|
||||
);
|
||||
|
||||
// Calcular time_to_sell para anúncios vendidos
|
||||
@@ -650,7 +651,6 @@ export async function aggregateHourlyMetrics(): Promise<void> {
|
||||
AND l.sold_at IS NOT NULL
|
||||
AND aa.time_to_sell IS NULL
|
||||
AND l.sold_at >= NOW() - INTERVAL '1 hour'
|
||||
`
|
||||
`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ export interface UpdateTransactionStatusData {
|
||||
* Cria uma nova transação
|
||||
*/
|
||||
export async function createTransaction(
|
||||
data: CreateTransactionData
|
||||
data: CreateTransactionData,
|
||||
): Promise<Transaction> {
|
||||
// Calcular escrow_fee se não fornecido (2% do valor)
|
||||
const escrowFee = data.escrow_fee ?? Number((data.amount * 0.02).toFixed(2));
|
||||
@@ -85,7 +85,7 @@ export async function createTransaction(
|
||||
data.amount,
|
||||
data.payment_method,
|
||||
escrowFee,
|
||||
]
|
||||
],
|
||||
);
|
||||
|
||||
return result.rows[0];
|
||||
@@ -95,13 +95,13 @@ export async function createTransaction(
|
||||
* Busca transação por ID
|
||||
*/
|
||||
export async function getTransactionById(
|
||||
transactionId: number
|
||||
transactionId: number,
|
||||
): Promise<Transaction | null> {
|
||||
const result = await query<Transaction>(
|
||||
`
|
||||
SELECT * FROM transactions WHERE transaction_id = $1
|
||||
`,
|
||||
[transactionId]
|
||||
[transactionId],
|
||||
);
|
||||
|
||||
return result.rows[0] || null;
|
||||
@@ -112,7 +112,7 @@ export async function getTransactionById(
|
||||
*/
|
||||
export async function updateTransactionStatus(
|
||||
transactionId: number,
|
||||
data: UpdateTransactionStatusData
|
||||
data: UpdateTransactionStatusData,
|
||||
): Promise<Transaction | null> {
|
||||
const updates: string[] = [];
|
||||
const params: any[] = [];
|
||||
@@ -172,7 +172,7 @@ export async function updateTransactionStatus(
|
||||
WHERE transaction_id = $${paramIndex}
|
||||
RETURNING *
|
||||
`,
|
||||
params
|
||||
params,
|
||||
);
|
||||
|
||||
return result.rows[0] || null;
|
||||
@@ -188,7 +188,7 @@ export async function getTransactionsByUser(
|
||||
status?: Transaction["transaction_status"];
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
} = {}
|
||||
} = {},
|
||||
): Promise<{ transactions: Transaction[]; total: number }> {
|
||||
const { role = "both", status, page = 1, pageSize = 20 } = filters;
|
||||
|
||||
@@ -201,7 +201,9 @@ export async function getTransactionsByUser(
|
||||
} else if (role === "seller") {
|
||||
conditions.push(`seller_id = $${paramIndex}`);
|
||||
} else {
|
||||
conditions.push(`(buyer_id = $${paramIndex} OR seller_id = $${paramIndex})`);
|
||||
conditions.push(
|
||||
`(buyer_id = $${paramIndex} OR seller_id = $${paramIndex})`,
|
||||
);
|
||||
}
|
||||
params.push(userId);
|
||||
paramIndex++;
|
||||
@@ -212,12 +214,13 @@ export async function getTransactionsByUser(
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
||||
const whereClause =
|
||||
conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
||||
|
||||
// Contar total
|
||||
const countResult = await query<{ count: string }>(
|
||||
`SELECT COUNT(*) as count FROM transactions ${whereClause}`,
|
||||
params
|
||||
params,
|
||||
);
|
||||
const total = parseInt(countResult.rows[0].count, 10);
|
||||
|
||||
@@ -230,7 +233,7 @@ export async function getTransactionsByUser(
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
|
||||
`,
|
||||
[...params, pageSize, offset]
|
||||
[...params, pageSize, offset],
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -238,4 +241,3 @@ export async function getTransactionsByUser(
|
||||
total,
|
||||
};
|
||||
}
|
||||
|
||||
+15
-12
@@ -45,9 +45,13 @@ export interface UpdateUserData {
|
||||
* Cria um novo usuário
|
||||
*/
|
||||
export async function createUser(
|
||||
data: CreateUserData
|
||||
data: CreateUserData,
|
||||
): Promise<{ userId: number; username: string; email: string }> {
|
||||
const result = await query<{ user_id: number; username: string; email: string }>(
|
||||
const result = await query<{
|
||||
user_id: number;
|
||||
username: string;
|
||||
email: string;
|
||||
}>(
|
||||
`
|
||||
INSERT INTO users (username, email, password_hash, first_name, last_name, phone, cpf)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
@@ -61,7 +65,7 @@ export async function createUser(
|
||||
data.last_name,
|
||||
data.phone || null,
|
||||
data.cpf || null,
|
||||
]
|
||||
],
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -79,7 +83,7 @@ export async function getUserByEmail(email: string): Promise<User | null> {
|
||||
`
|
||||
SELECT * FROM users WHERE email = $1
|
||||
`,
|
||||
[email]
|
||||
[email],
|
||||
);
|
||||
|
||||
return result.rows[0] || null;
|
||||
@@ -89,13 +93,13 @@ export async function getUserByEmail(email: string): Promise<User | null> {
|
||||
* Busca usuário por username
|
||||
*/
|
||||
export async function getUserByUsername(
|
||||
username: string
|
||||
username: string,
|
||||
): Promise<User | null> {
|
||||
const result = await query<User>(
|
||||
`
|
||||
SELECT * FROM users WHERE username = $1
|
||||
`,
|
||||
[username]
|
||||
[username],
|
||||
);
|
||||
|
||||
return result.rows[0] || null;
|
||||
@@ -114,7 +118,7 @@ export async function getUserById(userId: number): Promise<User | null> {
|
||||
FROM users
|
||||
WHERE user_id = $1
|
||||
`,
|
||||
[userId]
|
||||
[userId],
|
||||
);
|
||||
|
||||
return result.rows[0] || null;
|
||||
@@ -128,7 +132,7 @@ export async function getUserByCpf(cpf: string): Promise<User | null> {
|
||||
`
|
||||
SELECT user_id, username, email FROM users WHERE cpf = $1
|
||||
`,
|
||||
[cpf]
|
||||
[cpf],
|
||||
);
|
||||
|
||||
return result.rows[0] || null;
|
||||
@@ -139,7 +143,7 @@ export async function getUserByCpf(cpf: string): Promise<User | null> {
|
||||
*/
|
||||
export async function updateUser(
|
||||
userId: number,
|
||||
data: UpdateUserData
|
||||
data: UpdateUserData,
|
||||
): Promise<User> {
|
||||
const updates: string[] = [];
|
||||
const values: any[] = [];
|
||||
@@ -187,7 +191,7 @@ export async function updateUser(
|
||||
account_level, has_donated, whatsapp_notifications,
|
||||
created_at, updated_at, last_login, is_active, is_verified
|
||||
`,
|
||||
values
|
||||
values,
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
@@ -207,7 +211,6 @@ export async function updateLastLogin(userId: number): Promise<void> {
|
||||
SET last_login = CURRENT_TIMESTAMP
|
||||
WHERE user_id = $1
|
||||
`,
|
||||
[userId]
|
||||
[userId],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
* Lista completa de servidores oficiais
|
||||
*/
|
||||
|
||||
import { resolve } from "node:path";
|
||||
// Carregar variáveis de ambiente
|
||||
import { config } from "dotenv";
|
||||
import { resolve } from "path";
|
||||
|
||||
// Tentar carregar .env.local primeiro, depois .env
|
||||
config({ path: resolve(process.cwd(), ".env.local") });
|
||||
@@ -118,7 +118,7 @@ export async function seedServers() {
|
||||
// Verificar se já existe
|
||||
const existing = await query<{ server_id: number }>(
|
||||
"SELECT server_id FROM servers WHERE server_name = $1",
|
||||
[server.server_name]
|
||||
[server.server_name],
|
||||
);
|
||||
|
||||
if (existing.rows.length > 0) {
|
||||
@@ -130,16 +130,21 @@ export async function seedServers() {
|
||||
await query(
|
||||
`INSERT INTO servers (server_name, server_type, location, is_active)
|
||||
VALUES ($1, $2, $3, $4)`,
|
||||
[server.server_name, server.server_type, server.location, true]
|
||||
[server.server_name, server.server_type, server.location, true],
|
||||
);
|
||||
|
||||
inserted++;
|
||||
} catch (error) {
|
||||
console.error(`❌ Erro ao inserir servidor ${server.server_name}:`, error);
|
||||
console.error(
|
||||
`❌ Erro ao inserir servidor ${server.server_name}:`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✅ Seed concluído: ${inserted} servidores inseridos, ${skipped} já existiam\n`);
|
||||
console.log(
|
||||
`✅ Seed concluído: ${inserted} servidores inseridos, ${skipped} já existiam\n`,
|
||||
);
|
||||
}
|
||||
|
||||
// Se executado diretamente
|
||||
@@ -154,4 +159,3 @@ if (require.main === module) {
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
* Executa um health check e tenta uma query simples
|
||||
*/
|
||||
|
||||
import { resolve } from "node:path";
|
||||
// Carregar variáveis de ambiente
|
||||
import { config } from "dotenv";
|
||||
import { resolve } from "path";
|
||||
|
||||
// Tentar carregar .env.local primeiro, depois .env
|
||||
config({ path: resolve(process.cwd(), ".env.local") });
|
||||
config({ path: resolve(process.cwd(), ".env") });
|
||||
|
||||
import { healthCheck, query, closePool } from "./index";
|
||||
import { closePool, healthCheck, query } from "./index";
|
||||
|
||||
async function testConnection() {
|
||||
console.log("🔍 Testando conexão com PostgreSQL...\n");
|
||||
@@ -27,7 +27,9 @@ async function testConnection() {
|
||||
}
|
||||
|
||||
console.log("✅ Variável PG_DB encontrada");
|
||||
console.log(` Host: ${process.env.PG_DB.split("@")[1]?.split("/")[0] || "não detectado"}\n`);
|
||||
console.log(
|
||||
` Host: ${process.env.PG_DB.split("@")[1]?.split("/")[0] || "não detectado"}\n`,
|
||||
);
|
||||
|
||||
try {
|
||||
// Health check
|
||||
@@ -44,9 +46,11 @@ async function testConnection() {
|
||||
// Query de teste
|
||||
console.log("📊 Executando query de teste...");
|
||||
const result = await query("SELECT version(), NOW() as current_time");
|
||||
|
||||
|
||||
console.log("✅ Query executada com sucesso");
|
||||
console.log(` PostgreSQL Version: ${result.rows[0]?.version?.split(" ")[0] || "N/A"}`);
|
||||
console.log(
|
||||
` PostgreSQL Version: ${result.rows[0]?.version?.split(" ")[0] || "N/A"}`,
|
||||
);
|
||||
console.log(` Current Time: ${result.rows[0]?.current_time || "N/A"}\n`);
|
||||
|
||||
// Verificar se as tabelas existem
|
||||
@@ -76,14 +80,14 @@ async function testConnection() {
|
||||
} catch (error) {
|
||||
console.error("❌ Erro ao conectar com PostgreSQL:");
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
|
||||
|
||||
if (error instanceof Error && error.message.includes("timeout")) {
|
||||
console.log("\n💡 Verifique:");
|
||||
console.log(" - O servidor PostgreSQL está acessível?");
|
||||
console.log(" - A URL de conexão está correta?");
|
||||
console.log(" - O firewall permite conexões?");
|
||||
}
|
||||
|
||||
|
||||
if (error instanceof Error && error.message.includes("authentication")) {
|
||||
console.log("\n💡 Verifique:");
|
||||
console.log(" - Usuário e senha estão corretos?");
|
||||
@@ -96,4 +100,3 @@ async function testConnection() {
|
||||
}
|
||||
|
||||
testConnection();
|
||||
|
||||
+29
-15
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useSocket } from "./use-socket";
|
||||
import { useSession } from "./use-session";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import type { Socket } from "socket.io-client";
|
||||
import { useSession } from "./use-session";
|
||||
import { useSocket } from "./use-socket";
|
||||
|
||||
export interface ChatMessage {
|
||||
_id: string;
|
||||
@@ -109,7 +109,9 @@ export function useChat(chatId: string | null): UseChatReturn {
|
||||
queryKey: ["chat-messages", chatId, page],
|
||||
queryFn: async () => {
|
||||
if (!chatId) return null;
|
||||
const res = await fetch(`/api/chats/${chatId}/messages?page=${page}&limit=50`);
|
||||
const res = await fetch(
|
||||
`/api/chats/${chatId}/messages?page=${page}&limit=50`,
|
||||
);
|
||||
if (!res.ok) throw new Error("Erro ao buscar mensagens");
|
||||
return res.json();
|
||||
},
|
||||
@@ -126,7 +128,9 @@ export function useChat(chatId: string | null): UseChatReturn {
|
||||
// Adicionar mensagens antigas no início
|
||||
setMessages((prev) => [...messagesData.messages, ...prev]);
|
||||
}
|
||||
setHasMore(messagesData.pagination.page < messagesData.pagination.totalPages);
|
||||
setHasMore(
|
||||
messagesData.pagination.page < messagesData.pagination.totalPages,
|
||||
);
|
||||
}
|
||||
}, [messagesData, page]);
|
||||
|
||||
@@ -139,25 +143,33 @@ export function useChat(chatId: string | null): UseChatReturn {
|
||||
|
||||
// Listener para novas mensagens
|
||||
const handleNewMessage = (data: { message: ChatMessage }) => {
|
||||
console.log("[CHAT-DEBUG] useChat: new_message recebido", { message: data.message });
|
||||
|
||||
console.log("[CHAT-DEBUG] useChat: new_message recebido", {
|
||||
message: data.message,
|
||||
});
|
||||
|
||||
// Verificar se a mensagem já existe para evitar duplicação
|
||||
setMessages((prev) => {
|
||||
const messageExists = prev.some((msg) => msg._id === data.message._id);
|
||||
if (messageExists) {
|
||||
console.log("[CHAT-DEBUG] Mensagem já existe, ignorando duplicação", { messageId: data.message._id });
|
||||
console.log("[CHAT-DEBUG] Mensagem já existe, ignorando duplicação", {
|
||||
messageId: data.message._id,
|
||||
});
|
||||
return prev;
|
||||
}
|
||||
return [...prev, data.message];
|
||||
});
|
||||
|
||||
|
||||
// Invalidar cache
|
||||
queryClient.invalidateQueries({ queryKey: ["chat-messages", chatId] });
|
||||
queryClient.invalidateQueries({ queryKey: ["chat", chatId] });
|
||||
};
|
||||
|
||||
// Listener para indicador de digitação
|
||||
const handleTyping = (data: { user_id: number; username: string; is_typing: boolean }) => {
|
||||
const handleTyping = (data: {
|
||||
user_id: number;
|
||||
username: string;
|
||||
is_typing: boolean;
|
||||
}) => {
|
||||
if (data.user_id !== user?.userId) {
|
||||
setOtherUserTyping(data.is_typing);
|
||||
}
|
||||
@@ -177,7 +189,11 @@ export function useChat(chatId: string | null): UseChatReturn {
|
||||
async (content: string) => {
|
||||
if (!chatId || !socket || !isConnected || !content.trim()) return;
|
||||
|
||||
console.log("[CHAT-DEBUG] sendMessage chamado", { chatId, content, socketConnected: isConnected });
|
||||
console.log("[CHAT-DEBUG] sendMessage chamado", {
|
||||
chatId,
|
||||
content,
|
||||
socketConnected: isConnected,
|
||||
});
|
||||
|
||||
setIsSending(true);
|
||||
try {
|
||||
@@ -202,7 +218,7 @@ export function useChat(chatId: string | null): UseChatReturn {
|
||||
setIsSending(false);
|
||||
}
|
||||
},
|
||||
[chatId, socket, isConnected, user]
|
||||
[chatId, socket, isConnected],
|
||||
);
|
||||
|
||||
// Função para carregar mais mensagens
|
||||
@@ -211,7 +227,6 @@ export function useChat(chatId: string | null): UseChatReturn {
|
||||
setPage((prev) => prev + 1);
|
||||
}, [hasMore, isLoadingMessages]);
|
||||
|
||||
|
||||
return {
|
||||
chat: chatData?.chat || null,
|
||||
messages,
|
||||
@@ -230,7 +245,7 @@ export function useChat(chatId: string | null): UseChatReturn {
|
||||
export function useChatTyping(
|
||||
socket: Socket | null,
|
||||
isConnected: boolean,
|
||||
chatId: string | null
|
||||
chatId: string | null,
|
||||
) {
|
||||
const [isTyping, setIsTyping] = useState(false);
|
||||
const typingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
@@ -272,4 +287,3 @@ export function useChatTyping(
|
||||
|
||||
return { handleTypingStart, handleTypingStop };
|
||||
}
|
||||
|
||||
@@ -131,4 +131,3 @@ export function useInvalidateListings() {
|
||||
|
||||
return invalidate;
|
||||
}
|
||||
|
||||
@@ -68,29 +68,33 @@ export function useMyPurchaseIntentions() {
|
||||
const queryParams = new URLSearchParams();
|
||||
queryParams.set("limit", "100");
|
||||
|
||||
const { data, isLoading, error, refetch } = useQuery<PurchaseIntentionsResponse>({
|
||||
queryKey: ["my-purchase-intentions"],
|
||||
queryFn: async () => {
|
||||
const res = await fetch(`/api/purchase-intentions/my?${queryParams.toString()}`);
|
||||
if (!res.ok) {
|
||||
if (res.status === 401) {
|
||||
throw new Error("Não autenticado");
|
||||
const { data, isLoading, error, refetch } =
|
||||
useQuery<PurchaseIntentionsResponse>({
|
||||
queryKey: ["my-purchase-intentions"],
|
||||
queryFn: async () => {
|
||||
const res = await fetch(
|
||||
`/api/purchase-intentions/my?${queryParams.toString()}`,
|
||||
);
|
||||
if (!res.ok) {
|
||||
if (res.status === 401) {
|
||||
throw new Error("Não autenticado");
|
||||
}
|
||||
throw new Error("Erro ao buscar intenções de compra");
|
||||
}
|
||||
throw new Error("Erro ao buscar intenções de compra");
|
||||
}
|
||||
return res.json();
|
||||
},
|
||||
staleTime: 1000 * 30, // 30 segundos
|
||||
refetchInterval: 1000 * 30, // Refetch a cada 30 segundos
|
||||
refetchOnWindowFocus: true,
|
||||
refetchOnReconnect: true,
|
||||
enabled: true, // Sempre habilitado (autenticação é verificada no backend)
|
||||
});
|
||||
return res.json();
|
||||
},
|
||||
staleTime: 1000 * 30, // 30 segundos
|
||||
refetchInterval: 1000 * 30, // Refetch a cada 30 segundos
|
||||
refetchOnWindowFocus: true,
|
||||
refetchOnReconnect: true,
|
||||
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
|
||||
intention.custom_offer_amount !== null &&
|
||||
intention.custom_offer_amount !== undefined
|
||||
? Number(intention.custom_offer_amount)
|
||||
: null,
|
||||
}));
|
||||
@@ -116,4 +120,3 @@ export function useInvalidatePurchaseIntentions() {
|
||||
|
||||
return invalidate;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
export interface Server {
|
||||
serverId: number;
|
||||
@@ -70,7 +70,8 @@ export function useServers(): UseServersReturn {
|
||||
setError(data.error || "Erro ao buscar servidores");
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : "Erro desconhecido";
|
||||
const errorMessage =
|
||||
err instanceof Error ? err.message : "Erro desconhecido";
|
||||
setError("Erro ao conectar com o servidor");
|
||||
console.error("Erro ao buscar servidores:", errorMessage, err);
|
||||
} finally {
|
||||
@@ -89,4 +90,3 @@ export function useServers(): UseServersReturn {
|
||||
refetch: fetchServers,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -62,4 +62,3 @@ export function useSession() {
|
||||
refreshSession: refetch,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"use client";
|
||||
|
||||
// Re-exportar o hook que usa React Query
|
||||
export { useSession, type SessionUser } from "./use-session-query";
|
||||
|
||||
export { type SessionUser, useSession } from "./use-session-query";
|
||||
+13
-11
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { io, Socket } from "socket.io-client";
|
||||
import { io, type Socket } from "socket.io-client";
|
||||
import { useSession } from "./use-session";
|
||||
|
||||
interface UseSocketReturn {
|
||||
@@ -39,15 +39,18 @@ export function useSocket(): UseSocketReturn {
|
||||
}
|
||||
|
||||
try {
|
||||
const newSocket = io(process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000", {
|
||||
path: "/api/socket",
|
||||
transports: ["websocket", "polling"],
|
||||
withCredentials: true, // Enviar cookies automaticamente
|
||||
reconnection: true,
|
||||
reconnectionDelay: 1000,
|
||||
reconnectionDelayMax: 5000,
|
||||
reconnectionAttempts: 5,
|
||||
});
|
||||
const newSocket = io(
|
||||
process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000",
|
||||
{
|
||||
path: "/api/socket",
|
||||
transports: ["websocket", "polling"],
|
||||
withCredentials: true, // Enviar cookies automaticamente
|
||||
reconnection: true,
|
||||
reconnectionDelay: 1000,
|
||||
reconnectionDelayMax: 5000,
|
||||
reconnectionAttempts: 5,
|
||||
},
|
||||
);
|
||||
|
||||
newSocket.on("connect", () => {
|
||||
setIsConnected(true);
|
||||
@@ -90,4 +93,3 @@ export function useSocket(): UseSocketReturn {
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
+19
-15
@@ -2,7 +2,7 @@
|
||||
* Hooks para tracking de telemetria
|
||||
*/
|
||||
|
||||
import { useEffect, useRef, useCallback } from "react";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { getTelemetry } from "../telemetry/MarketTelemetry";
|
||||
import { useSession } from "./use-session";
|
||||
|
||||
@@ -49,17 +49,23 @@ export function useMarketTelemetry() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const trackSearch = useCallback((query: string, filters?: Record<string, any>) => {
|
||||
telemetryRef.current.trackSearch(query, filters);
|
||||
}, []);
|
||||
const trackSearch = useCallback(
|
||||
(query: string, filters?: Record<string, any>) => {
|
||||
telemetryRef.current.trackSearch(query, filters);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const trackFilter = useCallback((filters: Record<string, any>) => {
|
||||
telemetryRef.current.trackFilterApplied(filters);
|
||||
}, []);
|
||||
|
||||
const trackFavorite = useCallback((adId: number, action: "favorite" | "unfavorite") => {
|
||||
telemetryRef.current.trackFavorite(adId, action);
|
||||
}, []);
|
||||
const trackFavorite = useCallback(
|
||||
(adId: number, action: "favorite" | "unfavorite") => {
|
||||
telemetryRef.current.trackFavorite(adId, action);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const trackPurchase = useCallback((adId: number, price: number) => {
|
||||
telemetryRef.current.trackPurchase(adId, price);
|
||||
@@ -77,13 +83,12 @@ export function useMarketTelemetry() {
|
||||
telemetryRef.current.trackAdClick(adId, position);
|
||||
}, []);
|
||||
|
||||
const trackCustomEvent = useCallback((
|
||||
eventType: string,
|
||||
adId?: number,
|
||||
metadata?: Record<string, any>
|
||||
) => {
|
||||
telemetryRef.current.trackCustomEvent(eventType, adId, metadata);
|
||||
}, []);
|
||||
const trackCustomEvent = useCallback(
|
||||
(eventType: string, adId?: number, metadata?: Record<string, any>) => {
|
||||
telemetryRef.current.trackCustomEvent(eventType, adId, metadata);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return {
|
||||
trackSearch,
|
||||
@@ -97,4 +102,3 @@ export function useMarketTelemetry() {
|
||||
telemetry: telemetryRef.current,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -48,4 +48,3 @@ export async function resizeImageToDataURL(
|
||||
height,
|
||||
};
|
||||
}
|
||||
|
||||
+17
-10
@@ -19,11 +19,14 @@ export const ptToEnCategoryMap: Record<string, string> = {
|
||||
|
||||
// Mapeamento inverso EN -> PT para resolver caminho das imagens
|
||||
export const enToPtCategoryMap: Record<string, string> = Object.entries(
|
||||
ptToEnCategoryMap
|
||||
).reduce((acc, [pt, en]) => {
|
||||
acc[en] = pt;
|
||||
return acc;
|
||||
}, {} as Record<string, string>);
|
||||
ptToEnCategoryMap,
|
||||
).reduce(
|
||||
(acc, [pt, en]) => {
|
||||
acc[en] = pt;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>,
|
||||
);
|
||||
|
||||
function normalizeFilenameSegment(value: string): string {
|
||||
// Normaliza para casar melhor com nomes de arquivo: remove acentos e espaços extras
|
||||
@@ -38,12 +41,15 @@ function normalizeFilenameSegment(value: string): string {
|
||||
* Constrói um caminho relativo para a imagem do item dentro de data/images.
|
||||
* Retorna um caminho relativo utilizável em fs ou para um handler que sirva arquivos estáticos.
|
||||
*/
|
||||
export function buildItemImagePath(categoryEn: string, fileName: string): string {
|
||||
export function buildItemImagePath(
|
||||
categoryEn: string,
|
||||
fileName: string,
|
||||
): string {
|
||||
const ptFolder = enToPtCategoryMap[categoryEn] ?? "outros";
|
||||
// Mantém o nome original caso já esteja correto; fallback para nome normalizado
|
||||
const safeFileName = fileName || "";
|
||||
const normalized = normalizeFilenameSegment(
|
||||
safeFileName.replace(/\.(gif|png|jpg|jpeg|webp)$/i, "")
|
||||
safeFileName.replace(/\.(gif|png|jpg|jpeg|webp)$/i, ""),
|
||||
);
|
||||
// Tenta .gif como padrão (ajuste se necessário)
|
||||
const finalFile = safeFileName || `${normalized}.gif`;
|
||||
@@ -54,10 +60,11 @@ export function buildItemImagePath(categoryEn: string, fileName: string): string
|
||||
* Resolve a URL pública para a imagem, caso exista um handler ou static serving configurado.
|
||||
* Por padrão, apontamos para um API proxy que servirá o arquivo do filesystem.
|
||||
*/
|
||||
export function buildPublicItemImageUrl(categoryEn: string, fileName: string): string {
|
||||
export function buildPublicItemImageUrl(
|
||||
categoryEn: string,
|
||||
fileName: string,
|
||||
): string {
|
||||
const relativePath = buildItemImagePath(categoryEn, fileName);
|
||||
// Ex.: /api/images?path=data/images/espadas/sword.gif
|
||||
return `/api/images?path=${encodeURIComponent(relativePath)}`;
|
||||
}
|
||||
|
||||
|
||||
+57
-30
@@ -30,7 +30,7 @@ function getDatabase() {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
Database = require("better-sqlite3");
|
||||
} catch (err) {
|
||||
} catch (_err) {
|
||||
// Retorna null para permitir fallback silencioso (API responde vazio ao invés de 500)
|
||||
return null as any;
|
||||
}
|
||||
@@ -41,7 +41,10 @@ function getDatabase() {
|
||||
}
|
||||
|
||||
// Tenta deduzir ordenação segura a partir do parâmetro de sort
|
||||
function getOrderBy(sort: SearchParams["sort"], vocation?: string): { sql: string; bindings: any[] } {
|
||||
function getOrderBy(
|
||||
sort: SearchParams["sort"],
|
||||
_vocation?: string,
|
||||
): { sql: string; bindings: any[] } {
|
||||
// Ordenação sem prioridade de vocação (filtro de voc estrito cuida apenas do WHERE)
|
||||
switch (sort) {
|
||||
case "level-asc":
|
||||
@@ -54,7 +57,6 @@ function getOrderBy(sort: SearchParams["sort"], vocation?: string): { sql: strin
|
||||
return { sql: "weight DESC, name ASC", bindings: [] };
|
||||
case "category":
|
||||
return { sql: "category ASC, name ASC", bindings: [] };
|
||||
case "name":
|
||||
default:
|
||||
return { sql: "name ASC", bindings: [] };
|
||||
}
|
||||
@@ -107,17 +109,22 @@ export function searchItems<T = any>(params: SearchParams): SearchResult<T> {
|
||||
bindings.push(...rarities.map((r) => r.toLowerCase()));
|
||||
}
|
||||
|
||||
const { sql: orderBy, bindings: orderBindings } = getOrderBy(sort, vocation);
|
||||
const { sql: orderBy, bindings: orderBindings } = getOrderBy(
|
||||
sort,
|
||||
vocation,
|
||||
);
|
||||
|
||||
// Seleciona de múltiplas tabelas (PT) unificando shape padronizado
|
||||
const allTables = db
|
||||
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name <> 'sqlite_sequence'")
|
||||
.prepare(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name <> 'sqlite_sequence'",
|
||||
)
|
||||
.all() as { name: string }[];
|
||||
// Filtra por categorias solicitadas (EN → PT); caso vazio, usa todas
|
||||
const allowedPt = new Set(
|
||||
(categories.length > 0
|
||||
categories.length > 0
|
||||
? categories.map((en) => enToPtCategoryMap[en] || en)
|
||||
: allTables.map((t) => t.name))
|
||||
: allTables.map((t) => t.name),
|
||||
);
|
||||
const candidateTables = allTables
|
||||
.map((t) => t.name)
|
||||
@@ -128,9 +135,16 @@ export function searchItems<T = any>(params: SearchParams): SearchResult<T> {
|
||||
for (const t of candidateTables) {
|
||||
try {
|
||||
const cols = db.prepare(`PRAGMA table_info(${t})`).all() as any[];
|
||||
const colNames = new Set(cols.map((c) => (c?.name || "").toLowerCase()));
|
||||
const colNames = new Set(
|
||||
cols.map((c) => (c?.name || "").toLowerCase()),
|
||||
);
|
||||
// Requer colunas mínimas
|
||||
if (!colNames.has("id") || !colNames.has("nome") || !colNames.has("image_url")) continue;
|
||||
if (
|
||||
!colNames.has("id") ||
|
||||
!colNames.has("nome") ||
|
||||
!colNames.has("image_url")
|
||||
)
|
||||
continue;
|
||||
// Se filtro de vocação foi solicitado e a tabela não possui coluna 'voc', ignora esta tabela
|
||||
if (vocation && !colNames.has("voc")) {
|
||||
continue;
|
||||
@@ -147,16 +161,22 @@ export function searchItems<T = any>(params: SearchParams): SearchResult<T> {
|
||||
tableWhere.push("REPLACE(LOWER(voc), ' ', '') = REPLACE(?, ' ', '')");
|
||||
unionBindings.push(sel);
|
||||
}
|
||||
const whereSqlTbl = tableWhere.length ? `WHERE ${tableWhere.join(" AND ")}` : "";
|
||||
const whereSqlTbl = tableWhere.length
|
||||
? `WHERE ${tableWhere.join(" AND ")}`
|
||||
: "";
|
||||
// category: derivamos do nome da tabela (pt)
|
||||
const levelCol = colNames.has("nivel") || colNames.has("lvl")
|
||||
? `COALESCE(${colNames.has("nivel") ? "nivel" : "NULL"}, ${colNames.has("lvl") ? "lvl" : "NULL"})`
|
||||
: "NULL";
|
||||
const weightCol = colNames.has("peso") || colNames.has("peso_em_oz")
|
||||
? `COALESCE(${colNames.has("peso") ? "peso" : "NULL"}, ${colNames.has("peso_em_oz") ? "peso_em_oz" : "NULL"})`
|
||||
: "NULL";
|
||||
const levelCol =
|
||||
colNames.has("nivel") || colNames.has("lvl")
|
||||
? `COALESCE(${colNames.has("nivel") ? "nivel" : "NULL"}, ${colNames.has("lvl") ? "lvl" : "NULL"})`
|
||||
: "NULL";
|
||||
const weightCol =
|
||||
colNames.has("peso") || colNames.has("peso_em_oz")
|
||||
? `COALESCE(${colNames.has("peso") ? "peso" : "NULL"}, ${colNames.has("peso_em_oz") ? "peso_em_oz" : "NULL"})`
|
||||
: "NULL";
|
||||
const slotsCol = colNames.has("slots") ? "slots" : "NULL";
|
||||
const skillCol = colNames.has("skill_para_usar") ? "skill_para_usar" : "NULL";
|
||||
const skillCol = colNames.has("skill_para_usar")
|
||||
? "skill_para_usar"
|
||||
: "NULL";
|
||||
// Campos extras opcionais por tabela (ex.: aljavas)
|
||||
const extraCols = [
|
||||
"lvl",
|
||||
@@ -178,8 +198,8 @@ export function searchItems<T = any>(params: SearchParams): SearchResult<T> {
|
||||
${levelCol} AS level,
|
||||
${weightCol} AS weight,
|
||||
${slotsCol} AS slots,
|
||||
${skillCol} AS skill${extraSelect ? ",\n " + extraSelect : ""}
|
||||
FROM ${t} ${whereSqlTbl}`
|
||||
${skillCol} AS skill${extraSelect ? `,\n ${extraSelect}` : ""}
|
||||
FROM ${t} ${whereSqlTbl}`,
|
||||
);
|
||||
} catch (e) {
|
||||
console.warn(`[items-db] Ignorando tabela ${t}:`, (e as any)?.message);
|
||||
@@ -193,12 +213,14 @@ export function searchItems<T = any>(params: SearchParams): SearchResult<T> {
|
||||
|
||||
const unionSql = selects.join(" UNION ALL ");
|
||||
// Contagem total
|
||||
const totalStmt = db.prepare(`SELECT COUNT(1) as cnt FROM (${unionSql}) AS u`);
|
||||
const totalStmt = db.prepare(
|
||||
`SELECT COUNT(1) as cnt FROM (${unionSql}) AS u`,
|
||||
);
|
||||
const totalRow = totalStmt.get(...unionBindings) as { cnt: number };
|
||||
const total = totalRow?.cnt ?? 0;
|
||||
|
||||
const selectStmt = db.prepare(
|
||||
`SELECT * FROM (${unionSql}) AS u ORDER BY ${orderBy} LIMIT ? OFFSET ?`
|
||||
`SELECT * FROM (${unionSql}) AS u ORDER BY ${orderBy} LIMIT ? OFFSET ?`,
|
||||
);
|
||||
const items = selectStmt.all(...unionBindings, safePageSize, offset) as T[];
|
||||
|
||||
@@ -225,28 +247,33 @@ export function getItemById(itemId: number, itemName?: string): any | null {
|
||||
try {
|
||||
// Buscar em todas as tabelas de categorias
|
||||
const tables = db
|
||||
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
|
||||
.prepare(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'",
|
||||
)
|
||||
.all() as { name: string }[];
|
||||
|
||||
for (const table of tables) {
|
||||
try {
|
||||
let item: any = null;
|
||||
|
||||
|
||||
// Se itemName foi fornecido, buscar por ID E NOME (case-insensitive)
|
||||
if (itemName) {
|
||||
item = db.prepare(`SELECT * FROM ${table.name} WHERE id = ? AND LOWER(nome) = LOWER(?)`).get(itemId, itemName);
|
||||
item = db
|
||||
.prepare(
|
||||
`SELECT * FROM ${table.name} WHERE id = ? AND LOWER(nome) = LOWER(?)`,
|
||||
)
|
||||
.get(itemId, itemName);
|
||||
} else {
|
||||
// Se não há nome, buscar apenas por ID (comportamento antigo)
|
||||
item = db.prepare(`SELECT * FROM ${table.name} WHERE id = ?`).get(itemId);
|
||||
item = db
|
||||
.prepare(`SELECT * FROM ${table.name} WHERE id = ?`)
|
||||
.get(itemId);
|
||||
}
|
||||
|
||||
|
||||
if (item) {
|
||||
return item;
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignorar erros de tabelas que não têm coluna 'id' ou 'nome'
|
||||
continue;
|
||||
}
|
||||
} catch (_err) {}
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -52,9 +52,12 @@ function loadBetterSqlite(): any | null {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
return require("better-sqlite3");
|
||||
} catch (error) {
|
||||
secureLogger.warn("better-sqlite3 não está disponível. Importação automática será ignorada.", {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
secureLogger.warn(
|
||||
"better-sqlite3 não está disponível. Importação automática será ignorada.",
|
||||
{
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -62,14 +65,19 @@ function loadBetterSqlite(): any | null {
|
||||
function openSQLiteDatabase(Database: any) {
|
||||
const dbPath = path.join(process.cwd(), "data", "tibiawiki.db");
|
||||
if (!fs.existsSync(dbPath)) {
|
||||
secureLogger.warn("Base SQLite não encontrada em data/tibiawiki.db. Importação ignorada.");
|
||||
secureLogger.warn(
|
||||
"Base SQLite não encontrada em data/tibiawiki.db. Importação ignorada.",
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return new Database(dbPath, { readonly: true, fileMustExist: true });
|
||||
} catch (error) {
|
||||
secureLogger.error("Falha ao abrir base SQLite", error instanceof Error ? error.message : error);
|
||||
secureLogger.error(
|
||||
"Falha ao abrir base SQLite",
|
||||
error instanceof Error ? error.message : error,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -87,7 +95,9 @@ function toNullableDecimal(value: unknown): Prisma.Decimal | null {
|
||||
return new Prisma.Decimal(parsed);
|
||||
}
|
||||
|
||||
function collectMetadata(row: Record<string, unknown>): Record<string, unknown> {
|
||||
function collectMetadata(
|
||||
row: Record<string, unknown>,
|
||||
): Record<string, unknown> {
|
||||
const metadata: Record<string, unknown> = {};
|
||||
|
||||
for (const field of SQLITE_EXTRA_FIELDS) {
|
||||
@@ -125,7 +135,9 @@ export async function importItemsFromSQLite(): Promise<ImportStats> {
|
||||
|
||||
try {
|
||||
const tables = db
|
||||
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
|
||||
.prepare(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'",
|
||||
)
|
||||
.all() as { name: string }[];
|
||||
|
||||
let processedTables = 0;
|
||||
@@ -136,7 +148,9 @@ export async function importItemsFromSQLite(): Promise<ImportStats> {
|
||||
const batch: Prisma.ItemCreateManyInput[] = [];
|
||||
|
||||
for (const { name: tableName } of tables) {
|
||||
const pragma = db.prepare(`PRAGMA table_info(${tableName})`).all() as Array<{
|
||||
const pragma = db
|
||||
.prepare(`PRAGMA table_info(${tableName})`)
|
||||
.all() as Array<{
|
||||
name: string;
|
||||
}>;
|
||||
const colNames = new Set(pragma.map((col) => col.name.toLowerCase()));
|
||||
@@ -149,7 +163,10 @@ export async function importItemsFromSQLite(): Promise<ImportStats> {
|
||||
}
|
||||
|
||||
processedTables += 1;
|
||||
const rows = db.prepare(`SELECT * FROM ${tableName}`).all() as Record<string, unknown>[];
|
||||
const rows = db.prepare(`SELECT * FROM ${tableName}`).all() as Record<
|
||||
string,
|
||||
unknown
|
||||
>[];
|
||||
|
||||
for (const row of rows) {
|
||||
processedItems += 1;
|
||||
@@ -169,16 +186,20 @@ export async function importItemsFromSQLite(): Promise<ImportStats> {
|
||||
}
|
||||
|
||||
const imageUrl = (row.image_url ?? row.image ?? null) as string | null;
|
||||
const imageLocalPath = (row.image_local_path ?? row.image_local ?? null) as string | null;
|
||||
const imageLocalPath = (row.image_local_path ??
|
||||
row.image_local ??
|
||||
null) as string | null;
|
||||
const rarity =
|
||||
(row.rarity ?? row.raridade ?? row.rareza ?? null) !== null
|
||||
? String(row.rarity ?? row.raridade ?? row.rareza)
|
||||
: null;
|
||||
const level = toNullableInt(row.lvl ?? row.nivel ?? row.level);
|
||||
const weight = toNullableDecimal(row.peso ?? row.peso_em_oz ?? row.weight);
|
||||
const weight = toNullableDecimal(
|
||||
row.peso ?? row.peso_em_oz ?? row.weight,
|
||||
);
|
||||
const slots = toNullableInt(row.slots ?? row.slot);
|
||||
const skill =
|
||||
row.skill_para_usar ?? row.skill_required ?? row.skill ?? null
|
||||
(row.skill_para_usar ?? row.skill_required ?? row.skill ?? null)
|
||||
? String(row.skill_para_usar ?? row.skill_required ?? row.skill)
|
||||
: null;
|
||||
|
||||
@@ -240,14 +261,18 @@ export async function ensureItemsSeeded(): Promise<ImportStats | null> {
|
||||
const currentCount = await prisma.item.count();
|
||||
if (!seedLogged) {
|
||||
seedLogged = true;
|
||||
secureLogger.info("Verificação da tabela items concluída", { existing: currentCount });
|
||||
secureLogger.info("Verificação da tabela items concluída", {
|
||||
existing: currentCount,
|
||||
});
|
||||
}
|
||||
|
||||
if (currentCount > 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
secureLogger.info("Tabela items vazia. Iniciando importação automática a partir do SQLite.");
|
||||
secureLogger.info(
|
||||
"Tabela items vazia. Iniciando importação automática a partir do SQLite.",
|
||||
);
|
||||
try {
|
||||
const stats = await importItemsFromSQLite();
|
||||
secureLogger.info("Importação de itens concluída", stats);
|
||||
@@ -264,4 +289,3 @@ export async function ensureItemsSeeded(): Promise<ImportStats | null> {
|
||||
seedPromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
+37
-10
@@ -19,7 +19,13 @@ export interface CategoryTemplate {
|
||||
}
|
||||
|
||||
const BASE_COLUMNS: SQLiteColumnDefinition[] = [
|
||||
{ name: "id", type: "INTEGER", primaryKey: true, notNull: true, editable: false },
|
||||
{
|
||||
name: "id",
|
||||
type: "INTEGER",
|
||||
primaryKey: true,
|
||||
notNull: true,
|
||||
editable: false,
|
||||
},
|
||||
{ name: "nome", type: "TEXT", notNull: true, editable: true },
|
||||
{ name: "image_url", type: "TEXT", editable: true },
|
||||
{ name: "image_local_path", type: "TEXT", editable: true },
|
||||
@@ -50,7 +56,9 @@ function getDatabase(): any {
|
||||
return cachedDatabase;
|
||||
}
|
||||
|
||||
function normalizeColumnType(type: SQLiteColumnType | string): SQLiteColumnType {
|
||||
function normalizeColumnType(
|
||||
type: SQLiteColumnType | string,
|
||||
): SQLiteColumnType {
|
||||
const upper = type.toUpperCase();
|
||||
switch (upper) {
|
||||
case "INTEGER":
|
||||
@@ -82,7 +90,9 @@ function ensureImageDirectory(category: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function mapPragmaColumns(columns: Array<{ name: string; type: string; notnull: number; pk: number }>): SQLiteColumnDefinition[] {
|
||||
function mapPragmaColumns(
|
||||
columns: Array<{ name: string; type: string; notnull: number; pk: number }>,
|
||||
): SQLiteColumnDefinition[] {
|
||||
const mapped = columns.map((column) => {
|
||||
const baseMatch = BASE_COLUMNS.find((base) => base.name === column.name);
|
||||
return {
|
||||
@@ -100,11 +110,15 @@ function mapPragmaColumns(columns: Array<{ name: string; type: string; notnull:
|
||||
export function listCategoryTemplates(): CategoryTemplate[] {
|
||||
const db = getDatabase();
|
||||
const tables = db
|
||||
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
|
||||
.prepare(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'",
|
||||
)
|
||||
.all() as Array<{ name: string }>;
|
||||
|
||||
return tables.map((table) => {
|
||||
const pragma = db.prepare(`PRAGMA table_info(${table.name})`).all() as Array<{
|
||||
const pragma = db
|
||||
.prepare(`PRAGMA table_info(${table.name})`)
|
||||
.all() as Array<{
|
||||
name: string;
|
||||
type: string;
|
||||
notnull: number;
|
||||
@@ -124,7 +138,9 @@ export function listCategoryTemplates(): CategoryTemplate[] {
|
||||
|
||||
function validateCategoryName(name: string) {
|
||||
if (!/^[a-z0-9_]+$/i.test(name)) {
|
||||
throw new Error("Nome da categoria deve conter apenas letras, números ou sublinhado.");
|
||||
throw new Error(
|
||||
"Nome da categoria deve conter apenas letras, números ou sublinhado.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +149,9 @@ export interface CreateCategoryInput {
|
||||
columns?: SQLiteColumnDefinition[];
|
||||
}
|
||||
|
||||
export function createCategoryTemplate(input: CreateCategoryInput): CategoryTemplate {
|
||||
export function createCategoryTemplate(
|
||||
input: CreateCategoryInput,
|
||||
): CategoryTemplate {
|
||||
const db = getDatabase();
|
||||
const name = input.name.trim().toLowerCase();
|
||||
validateCategoryName(name);
|
||||
@@ -184,7 +202,9 @@ export interface AddColumnInput {
|
||||
column: SQLiteColumnDefinition;
|
||||
}
|
||||
|
||||
export function addColumnToCategory(input: AddColumnInput): SQLiteColumnDefinition {
|
||||
export function addColumnToCategory(
|
||||
input: AddColumnInput,
|
||||
): SQLiteColumnDefinition {
|
||||
const db = getDatabase();
|
||||
const name = input.category.trim().toLowerCase();
|
||||
validateCategoryName(name);
|
||||
@@ -262,7 +282,11 @@ export function insertCategoryItem(input: UpsertItemInput): number {
|
||||
return Number(result.lastInsertRowid);
|
||||
}
|
||||
|
||||
export function updateCategoryItem(category: string, id: number, data: Record<string, unknown>) {
|
||||
export function updateCategoryItem(
|
||||
category: string,
|
||||
id: number,
|
||||
data: Record<string, unknown>,
|
||||
) {
|
||||
const db = getDatabase();
|
||||
const normalized = category.trim().toLowerCase();
|
||||
validateCategoryName(normalized);
|
||||
@@ -282,7 +306,10 @@ export function updateCategoryItem(category: string, id: number, data: Record<st
|
||||
db.prepare(sql).run(...values, id);
|
||||
}
|
||||
|
||||
export function getCategoryItem(category: string, id: number): Record<string, unknown> | null {
|
||||
export function getCategoryItem(
|
||||
category: string,
|
||||
id: number,
|
||||
): Record<string, unknown> | null {
|
||||
const db = getDatabase();
|
||||
const normalized = category.trim().toLowerCase();
|
||||
validateCategoryName(normalized);
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { TibiaItem, MarketOffer } from "@/types/item";
|
||||
import type { MarketOffer, TibiaItem } from "@/types/item";
|
||||
|
||||
export const featuredItems: TibiaItem[] = [
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Singleton pattern para reutilizar conexão
|
||||
*/
|
||||
|
||||
import mongoose from "mongoose";
|
||||
import type mongoose from "mongoose";
|
||||
|
||||
function getMongoUrl(): string {
|
||||
const url = process.env.MONGO_URL;
|
||||
@@ -24,7 +24,7 @@ declare global {
|
||||
var mongoose: MongooseCache | undefined;
|
||||
}
|
||||
|
||||
let cached: MongooseCache = global.mongoose || { conn: null, promise: null };
|
||||
const cached: MongooseCache = global.mongoose || { conn: null, promise: null };
|
||||
|
||||
if (!global.mongoose) {
|
||||
global.mongoose = cached;
|
||||
@@ -79,4 +79,3 @@ export async function disconnectMongoDB(): Promise<void> {
|
||||
cached.promise = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Model Chat - Representa uma conversa entre comprador e vendedor
|
||||
*/
|
||||
|
||||
import mongoose, { Schema, model, models } from "mongoose";
|
||||
import { model, models, Schema } from "mongoose";
|
||||
|
||||
export interface IChat {
|
||||
chat_id: string; // ID único: `${listing_id}_${buyer_id}_${seller_id}`
|
||||
@@ -16,7 +16,7 @@ export interface IChat {
|
||||
buyer_unread_count: number;
|
||||
seller_unread_count: number;
|
||||
is_active: boolean; // Chat ativo (anúncio ainda disponível)
|
||||
|
||||
|
||||
// Dados cached do anúncio (para performance)
|
||||
listing_cache: {
|
||||
item_name: string;
|
||||
@@ -27,14 +27,14 @@ export interface IChat {
|
||||
server_name: string;
|
||||
listing_status: string;
|
||||
};
|
||||
|
||||
|
||||
// Dados cached dos usuários
|
||||
buyer_cache: {
|
||||
user_id: number;
|
||||
username: string;
|
||||
character_name?: string;
|
||||
};
|
||||
|
||||
|
||||
seller_cache: {
|
||||
user_id: number;
|
||||
username: string;
|
||||
@@ -120,7 +120,7 @@ const ChatSchema = new Schema<IChat>(
|
||||
{
|
||||
timestamps: false, // Usamos campos manuais
|
||||
collection: "chats",
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Índices compostos para queries eficientes
|
||||
@@ -135,4 +135,3 @@ ChatSchema.pre("save", function (next) {
|
||||
});
|
||||
|
||||
export const Chat = models.Chat || model<IChat>("Chat", ChatSchema);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Model Message - Representa uma mensagem em um chat
|
||||
*/
|
||||
|
||||
import mongoose, { Schema, model, models } from "mongoose";
|
||||
import { model, models, Schema } from "mongoose";
|
||||
|
||||
export interface IMessage {
|
||||
chat_id: string; // Referência ao chat
|
||||
@@ -12,7 +12,7 @@ export interface IMessage {
|
||||
read_at: Date | null; // Quando foi lida
|
||||
edited_at: Date | null; // Se foi editada
|
||||
deleted_at: Date | null; // Soft delete
|
||||
|
||||
|
||||
// Metadados
|
||||
message_type: "text" | "system"; // System para mensagens automáticas
|
||||
metadata?: {
|
||||
@@ -69,7 +69,7 @@ const MessageSchema = new Schema<IMessage>(
|
||||
{
|
||||
timestamps: false, // Usamos campos manuais
|
||||
collection: "messages",
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Índices compostos para queries eficientes
|
||||
@@ -77,5 +77,5 @@ MessageSchema.index({ chat_id: 1, created_at: -1 }); // Para buscar mensagens de
|
||||
MessageSchema.index({ chat_id: 1, sender_id: 1, created_at: -1 });
|
||||
MessageSchema.index({ sender_id: 1, created_at: -1 });
|
||||
|
||||
export const Message = models.Message || model<IMessage>("Message", MessageSchema);
|
||||
|
||||
export const Message =
|
||||
models.Message || model<IMessage>("Message", MessageSchema);
|
||||
@@ -18,7 +18,7 @@ export function markAsPaid(
|
||||
amount: number;
|
||||
anonymous: boolean;
|
||||
customer?: any;
|
||||
}
|
||||
},
|
||||
) {
|
||||
paymentCache.set(pixId, {
|
||||
paid: true,
|
||||
|
||||
@@ -7,7 +7,7 @@ export function confirmPayment(pixId: string) {
|
||||
try {
|
||||
// Obter array de pixIds confirmados
|
||||
const confirmedStorage = localStorage.getItem("confirmedPixIds");
|
||||
let confirmed: string[] = confirmedStorage
|
||||
const confirmed: string[] = confirmedStorage
|
||||
? JSON.parse(confirmedStorage)
|
||||
: [];
|
||||
|
||||
|
||||
@@ -18,4 +18,3 @@ export const prisma =
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
globalForPrisma.prisma = prisma;
|
||||
}
|
||||
|
||||
+42
-32
@@ -4,11 +4,11 @@
|
||||
* usamos $queryRaw para queries dinâmicas
|
||||
*/
|
||||
|
||||
import { Prisma } from "@prisma/client";
|
||||
// Nota: itemsPrisma não está disponível ainda - usando better-sqlite3 diretamente
|
||||
// TODO: Implementar quando migrar SQLite para Prisma
|
||||
// import { itemsPrisma } from "./items";
|
||||
import { enToPtCategoryMap } from "../item-images";
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
// Stub temporário - não está sendo usado no momento
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@@ -50,7 +50,6 @@ function getOrderBy(sort: SearchParams["sort"]): string {
|
||||
return "weight DESC, name ASC";
|
||||
case "category":
|
||||
return "category ASC, name ASC";
|
||||
case "name":
|
||||
default:
|
||||
return "name ASC";
|
||||
}
|
||||
@@ -60,7 +59,7 @@ function getOrderBy(sort: SearchParams["sort"]): string {
|
||||
* Busca itens no banco SQLite usando Prisma $queryRaw
|
||||
*/
|
||||
export async function searchItems<T = any>(
|
||||
params: SearchParams
|
||||
params: SearchParams,
|
||||
): Promise<SearchResult<T>> {
|
||||
const {
|
||||
q = "",
|
||||
@@ -80,27 +79,37 @@ export async function searchItems<T = any>(
|
||||
// Log básico do schema na primeira chamada
|
||||
if (!schemaLogged) {
|
||||
try {
|
||||
const tables = await (itemsPrisma.$queryRaw as any)(
|
||||
Prisma.raw(`SELECT name FROM sqlite_master WHERE type='table' ORDER BY name`)
|
||||
) as Array<{ name: string }>;
|
||||
console.info("[items-db] Tabelas:", tables.map((t: { name: string }) => t.name));
|
||||
const tables = (await (itemsPrisma.$queryRaw as any)(
|
||||
Prisma.raw(
|
||||
`SELECT name FROM sqlite_master WHERE type='table' ORDER BY name`,
|
||||
),
|
||||
)) as Array<{ name: string }>;
|
||||
console.info(
|
||||
"[items-db] Tabelas:",
|
||||
tables.map((t: { name: string }) => t.name),
|
||||
);
|
||||
schemaLogged = true;
|
||||
} catch (intErr) {
|
||||
console.warn("[items-db] Falha ao ler schema:", (intErr as any)?.message);
|
||||
console.warn(
|
||||
"[items-db] Falha ao ler schema:",
|
||||
(intErr as any)?.message,
|
||||
);
|
||||
schemaLogged = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Buscar todas as tabelas disponíveis
|
||||
const allTables = await (itemsPrisma.$queryRaw as any)(
|
||||
Prisma.raw(`SELECT name FROM sqlite_master WHERE type='table' AND name <> 'sqlite_sequence'`)
|
||||
) as Array<{ name: string }>;
|
||||
const allTables = (await (itemsPrisma.$queryRaw as any)(
|
||||
Prisma.raw(
|
||||
`SELECT name FROM sqlite_master WHERE type='table' AND name <> 'sqlite_sequence'`,
|
||||
),
|
||||
)) as Array<{ name: string }>;
|
||||
|
||||
// Filtra por categorias solicitadas (EN → PT)
|
||||
const allowedPt = new Set(
|
||||
categories.length > 0
|
||||
? categories.map((en) => enToPtCategoryMap[en] || en)
|
||||
: allTables.map((t: { name: string }) => t.name)
|
||||
: allTables.map((t: { name: string }) => t.name),
|
||||
);
|
||||
const candidateTables = allTables
|
||||
.map((t: { name: string }) => t.name)
|
||||
@@ -112,11 +121,13 @@ export async function searchItems<T = any>(
|
||||
for (const tableName of candidateTables) {
|
||||
try {
|
||||
// Verificar colunas da tabela
|
||||
const cols = await (itemsPrisma.$queryRaw as any)(
|
||||
Prisma.raw(`PRAGMA table_info(${tableName})`)
|
||||
) as Array<{ name: string; type: string }>;
|
||||
const cols = (await (itemsPrisma.$queryRaw as any)(
|
||||
Prisma.raw(`PRAGMA table_info(${tableName})`),
|
||||
)) as Array<{ name: string; type: string }>;
|
||||
|
||||
const colNames = new Set(cols.map((c: { name?: string }) => (c?.name || "").toLowerCase()));
|
||||
const colNames = new Set(
|
||||
cols.map((c: { name?: string }) => (c?.name || "").toLowerCase()),
|
||||
);
|
||||
|
||||
// Requer colunas mínimas
|
||||
if (
|
||||
@@ -184,13 +195,16 @@ export async function searchItems<T = any>(
|
||||
${levelCol} AS level,
|
||||
${weightCol} AS weight,
|
||||
${slotsCol} AS slots,
|
||||
${skillCol} AS skill${extraSelect ? ", " + extraSelect : ""}
|
||||
${skillCol} AS skill${extraSelect ? `, ${extraSelect}` : ""}
|
||||
FROM ${tableName} ${whereSqlTbl}`;
|
||||
|
||||
selects.push(selectSql);
|
||||
bindings.push(...tableBindings);
|
||||
} catch (e) {
|
||||
console.warn(`[items-db] Ignorando tabela ${tableName}:`, (e as any)?.message);
|
||||
console.warn(
|
||||
`[items-db] Ignorando tabela ${tableName}:`,
|
||||
(e as any)?.message,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,20 +218,20 @@ export async function searchItems<T = any>(
|
||||
|
||||
// Contagem total
|
||||
const countSql = `SELECT COUNT(1) as cnt FROM (${unionSql}) AS u`;
|
||||
const countResult = await (itemsPrisma.$queryRaw as any)(
|
||||
const countResult = (await (itemsPrisma.$queryRaw as any)(
|
||||
Prisma.raw(countSql),
|
||||
...bindings
|
||||
) as Array<{ cnt: number }>;
|
||||
...bindings,
|
||||
)) as Array<{ cnt: number }>;
|
||||
const total = countResult[0]?.cnt ?? 0;
|
||||
|
||||
// Buscar itens com paginação
|
||||
const selectSql = `SELECT * FROM (${unionSql}) AS u ORDER BY ${orderBy} LIMIT ? OFFSET ?`;
|
||||
const items = await (itemsPrisma.$queryRaw as any)(
|
||||
const items = (await (itemsPrisma.$queryRaw as any)(
|
||||
Prisma.raw(selectSql),
|
||||
...bindings,
|
||||
safePageSize,
|
||||
offset
|
||||
) as T[];
|
||||
offset,
|
||||
)) as T[];
|
||||
|
||||
return { items, total };
|
||||
} catch (error) {
|
||||
@@ -239,18 +253,15 @@ export async function getItemById(itemId: number): Promise<any | null> {
|
||||
|
||||
for (const table of tables) {
|
||||
try {
|
||||
const item = await (itemsPrisma.$queryRaw as any)(
|
||||
const item = (await (itemsPrisma.$queryRaw as any)(
|
||||
Prisma.raw(`SELECT * FROM ${table.name} WHERE id = ?`),
|
||||
itemId
|
||||
) as any[];
|
||||
itemId,
|
||||
)) as any[];
|
||||
|
||||
if (item && item.length > 0) {
|
||||
return item[0];
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignorar erros de tabelas que não têm coluna 'id' ou outros problemas
|
||||
continue;
|
||||
}
|
||||
} catch (_err) {}
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -259,4 +270,3 @@ export async function getItemById(itemId: number): Promise<any | null> {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
/**
|
||||
* Prisma Client para SQLite (Banco de Itens do TibiaWiki)
|
||||
*
|
||||
*
|
||||
* NOTA: Por enquanto, mantemos better-sqlite3 para queries SQLite
|
||||
* devido às limitações do Prisma com tabelas dinâmicas.
|
||||
*
|
||||
*
|
||||
* Este arquivo está preparado para migração futura quando necessário.
|
||||
*/
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ export async function createBid(data: CreateBidData): Promise<Bid> {
|
||||
return {
|
||||
...bid,
|
||||
bid_amount: Number(bid.bid_amount),
|
||||
bid_type: (bid.bid_type as "pix" | "tibia_coins" | "gold_coins" | null),
|
||||
bid_type: bid.bid_type as "pix" | "tibia_coins" | "gold_coins" | null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -46,22 +46,19 @@ export async function createBid(data: CreateBidData): Promise<Bid> {
|
||||
*/
|
||||
export async function getBidsByListing(
|
||||
listingId: number,
|
||||
activeOnly: boolean = true
|
||||
activeOnly: boolean = true,
|
||||
): Promise<Bid[]> {
|
||||
const bids = await prisma.bid.findMany({
|
||||
where: {
|
||||
listing_id: listingId,
|
||||
...(activeOnly ? { is_active: true } : {}),
|
||||
},
|
||||
orderBy: [
|
||||
{ bid_amount: "desc" },
|
||||
{ created_at: "desc" },
|
||||
],
|
||||
orderBy: [{ bid_amount: "desc" }, { created_at: "desc" }],
|
||||
});
|
||||
return bids.map(bid => ({
|
||||
return bids.map((bid) => ({
|
||||
...bid,
|
||||
bid_amount: Number(bid.bid_amount),
|
||||
bid_type: (bid.bid_type as "pix" | "tibia_coins" | "gold_coins" | null),
|
||||
bid_type: bid.bid_type as "pix" | "tibia_coins" | "gold_coins" | null,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -74,16 +71,13 @@ export async function getHighestBid(listingId: number): Promise<Bid | null> {
|
||||
listing_id: listingId,
|
||||
is_active: true,
|
||||
},
|
||||
orderBy: [
|
||||
{ bid_amount: "desc" },
|
||||
{ created_at: "desc" },
|
||||
],
|
||||
orderBy: [{ bid_amount: "desc" }, { created_at: "desc" }],
|
||||
});
|
||||
if (!bid) return null;
|
||||
return {
|
||||
...bid,
|
||||
bid_amount: Number(bid.bid_amount),
|
||||
bid_type: (bid.bid_type as "pix" | "tibia_coins" | "gold_coins" | null),
|
||||
bid_type: bid.bid_type as "pix" | "tibia_coins" | "gold_coins" | null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -110,7 +104,6 @@ export async function getBidById(bidId: number): Promise<Bid | null> {
|
||||
return {
|
||||
...bid,
|
||||
bid_amount: Number(bid.bid_amount),
|
||||
bid_type: (bid.bid_type as "pix" | "tibia_coins" | "gold_coins" | null),
|
||||
bid_type: bid.bid_type as "pix" | "tibia_coins" | "gold_coins" | null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export interface UpdateCharacterData {
|
||||
*/
|
||||
export async function createCharacter(
|
||||
userId: number,
|
||||
data: CreateCharacterData
|
||||
data: CreateCharacterData,
|
||||
): Promise<Character> {
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
// Se este personagem será main, desmarcar outros
|
||||
@@ -62,25 +62,22 @@ export async function createCharacter(
|
||||
export async function getUserCharacters(userId: number): Promise<Character[]> {
|
||||
return await prisma.userCharacter.findMany({
|
||||
where: { user_id: userId },
|
||||
orderBy: [
|
||||
{ is_main: "desc" },
|
||||
{ created_at: "asc" },
|
||||
],
|
||||
orderBy: [{ is_main: "desc" }, { created_at: "asc" }],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Busca personagem principal de um usuário
|
||||
*/
|
||||
export async function getMainCharacter(userId: number): Promise<Character | null> {
|
||||
export async function getMainCharacter(
|
||||
userId: number,
|
||||
): Promise<Character | null> {
|
||||
return await prisma.userCharacter.findFirst({
|
||||
where: {
|
||||
user_id: userId,
|
||||
is_main: true,
|
||||
},
|
||||
orderBy: [
|
||||
{ created_at: "asc" },
|
||||
],
|
||||
orderBy: [{ created_at: "asc" }],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -89,17 +86,14 @@ export async function getMainCharacter(userId: number): Promise<Character | null
|
||||
*/
|
||||
export async function getMainCharacterByUserId(
|
||||
userId: number,
|
||||
serverName: string
|
||||
serverName: string,
|
||||
): Promise<Character | null> {
|
||||
return await prisma.userCharacter.findFirst({
|
||||
where: {
|
||||
user_id: userId,
|
||||
server_name: serverName,
|
||||
},
|
||||
orderBy: [
|
||||
{ is_main: "desc" },
|
||||
{ created_at: "asc" },
|
||||
],
|
||||
orderBy: [{ is_main: "desc" }, { created_at: "asc" }],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -107,7 +101,7 @@ export async function getMainCharacterByUserId(
|
||||
* Busca personagem por ID
|
||||
*/
|
||||
export async function getCharacterById(
|
||||
characterId: number
|
||||
characterId: number,
|
||||
): Promise<Character | null> {
|
||||
return await prisma.userCharacter.findUnique({
|
||||
where: { character_id: characterId },
|
||||
@@ -121,7 +115,7 @@ export async function getCharacterById(
|
||||
export async function updateCharacter(
|
||||
characterId: number,
|
||||
userId: number,
|
||||
data: UpdateCharacterData
|
||||
data: UpdateCharacterData,
|
||||
): Promise<Character> {
|
||||
// Verificar ownership primeiro
|
||||
const existing = await getCharacterById(characterId);
|
||||
@@ -147,8 +141,10 @@ export async function updateCharacter(
|
||||
|
||||
// Atualizar personagem
|
||||
const updateData: any = {};
|
||||
if (data.character_name !== undefined) updateData.character_name = data.character_name;
|
||||
if (data.server_name !== undefined) updateData.server_name = data.server_name;
|
||||
if (data.character_name !== undefined)
|
||||
updateData.character_name = data.character_name;
|
||||
if (data.server_name !== undefined)
|
||||
updateData.server_name = data.server_name;
|
||||
if (data.is_main !== undefined) updateData.is_main = data.is_main;
|
||||
|
||||
if (Object.keys(updateData).length === 0) {
|
||||
@@ -168,7 +164,7 @@ export async function updateCharacter(
|
||||
*/
|
||||
export async function deleteCharacter(
|
||||
characterId: number,
|
||||
userId: number
|
||||
userId: number,
|
||||
): Promise<void> {
|
||||
// Verificar ownership
|
||||
const existing = await getCharacterById(characterId);
|
||||
@@ -184,4 +180,3 @@ export async function deleteCharacter(
|
||||
where: { character_id: characterId },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,7 +7,10 @@ import { prisma } from "../app";
|
||||
/**
|
||||
* Verifica se um anúncio está favoritado por um usuário
|
||||
*/
|
||||
export async function isFavorite(userId: number, listingId: number): Promise<boolean> {
|
||||
export async function isFavorite(
|
||||
userId: number,
|
||||
listingId: number,
|
||||
): Promise<boolean> {
|
||||
const favorite = await prisma.userFavorite.findUnique({
|
||||
where: {
|
||||
user_id_ad_id: {
|
||||
@@ -23,7 +26,10 @@ export async function isFavorite(userId: number, listingId: number): Promise<boo
|
||||
/**
|
||||
* Favorita um anúncio
|
||||
*/
|
||||
export async function addFavorite(userId: number, listingId: number): Promise<void> {
|
||||
export async function addFavorite(
|
||||
userId: number,
|
||||
listingId: number,
|
||||
): Promise<void> {
|
||||
await prisma.userFavorite.upsert({
|
||||
where: {
|
||||
user_id_ad_id: {
|
||||
@@ -42,23 +48,31 @@ export async function addFavorite(userId: number, listingId: number): Promise<vo
|
||||
/**
|
||||
* Remove favorito de um anúncio
|
||||
*/
|
||||
export async function removeFavorite(userId: number, listingId: number): Promise<void> {
|
||||
await prisma.userFavorite.delete({
|
||||
where: {
|
||||
user_id_ad_id: {
|
||||
user_id: userId,
|
||||
ad_id: listingId,
|
||||
export async function removeFavorite(
|
||||
userId: number,
|
||||
listingId: number,
|
||||
): Promise<void> {
|
||||
await prisma.userFavorite
|
||||
.delete({
|
||||
where: {
|
||||
user_id_ad_id: {
|
||||
user_id: userId,
|
||||
ad_id: listingId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}).catch(() => {
|
||||
// Ignorar erro se não existir (idempotente)
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
// Ignorar erro se não existir (idempotente)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle favorite (favorita se não estiver, desfavorita se estiver)
|
||||
*/
|
||||
export async function toggleFavorite(userId: number, listingId: number): Promise<boolean> {
|
||||
export async function toggleFavorite(
|
||||
userId: number,
|
||||
listingId: number,
|
||||
): Promise<boolean> {
|
||||
const isFavorited = await isFavorite(userId, listingId);
|
||||
|
||||
if (isFavorited) {
|
||||
@@ -69,4 +83,3 @@ export async function toggleFavorite(userId: number, listingId: number): Promise
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
+75
-35
@@ -4,9 +4,9 @@
|
||||
*/
|
||||
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { prisma } from "../app";
|
||||
import { ensureItemsSeeded } from "../../items/import-from-sqlite";
|
||||
import { secureLogger } from "../../secure-logger";
|
||||
import { prisma } from "../app";
|
||||
|
||||
export type ItemSort =
|
||||
| "name"
|
||||
@@ -96,13 +96,14 @@ function buildOrder(sort: ItemSort): Prisma.ItemOrderByWithRelationInput[] {
|
||||
return [{ category: "asc" }, { item_name: "asc" }];
|
||||
case "name":
|
||||
return [{ item_name: "asc" }];
|
||||
case "updated-desc":
|
||||
default:
|
||||
return [{ updated_at: "desc" }];
|
||||
}
|
||||
}
|
||||
|
||||
function parseMetadata(metadata: Prisma.JsonValue | null | undefined): Record<string, any> {
|
||||
function parseMetadata(
|
||||
metadata: Prisma.JsonValue | null | undefined,
|
||||
): Record<string, any> {
|
||||
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
|
||||
return {};
|
||||
}
|
||||
@@ -142,8 +143,14 @@ function extractTags(meta: Record<string, any>): string[] {
|
||||
}
|
||||
|
||||
function buildItem(raw: any): Item {
|
||||
const createdAt = raw.created_at instanceof Date ? raw.created_at.toISOString() : raw.created_at;
|
||||
const updatedAt = raw.updated_at instanceof Date ? raw.updated_at.toISOString() : raw.updated_at;
|
||||
const createdAt =
|
||||
raw.created_at instanceof Date
|
||||
? raw.created_at.toISOString()
|
||||
: raw.created_at;
|
||||
const updatedAt =
|
||||
raw.updated_at instanceof Date
|
||||
? raw.updated_at.toISOString()
|
||||
: raw.updated_at;
|
||||
|
||||
return {
|
||||
...raw,
|
||||
@@ -158,7 +165,10 @@ function buildItemWithMeta(raw: any): ItemWithMeta {
|
||||
const base = buildItem(raw);
|
||||
const servers = extractServers(base.metadata);
|
||||
const tags = extractTags(base.metadata);
|
||||
const description = typeof base.metadata.description === "string" ? base.metadata.description : null;
|
||||
const description =
|
||||
typeof base.metadata.description === "string"
|
||||
? base.metadata.description
|
||||
: null;
|
||||
|
||||
return {
|
||||
...base,
|
||||
@@ -177,7 +187,12 @@ function toDecimal(value: number | null | undefined): Prisma.Decimal | null {
|
||||
|
||||
function buildMetadata(
|
||||
existing: Record<string, any>,
|
||||
overrides: { servers?: string[]; tags?: string[]; description?: string | null; metadata?: Record<string, any> }
|
||||
overrides: {
|
||||
servers?: string[];
|
||||
tags?: string[];
|
||||
description?: string | null;
|
||||
metadata?: Record<string, any>;
|
||||
},
|
||||
): Record<string, any> {
|
||||
const merged = { ...existing, ...(overrides.metadata || {}) };
|
||||
|
||||
@@ -220,10 +235,15 @@ const FORBIDDEN_VALUE_KEYS = new Set<string>([
|
||||
* Verifica recursivamente se há campos proibidos relacionados a valores/preços.
|
||||
* Lança erro se encontrar.
|
||||
*/
|
||||
export function assertNoValueFieldsInMetadata(meta: Record<string, any> | undefined | null, context?: string): void {
|
||||
export function assertNoValueFieldsInMetadata(
|
||||
meta: Record<string, any> | undefined | null,
|
||||
context?: string,
|
||||
): void {
|
||||
if (!meta || typeof meta !== "object") return;
|
||||
|
||||
const stack: Array<{ obj: Record<string, any>; path: string }> = [{ obj: meta as Record<string, any>, path: "metadata" }];
|
||||
const stack: Array<{ obj: Record<string, any>; path: string }> = [
|
||||
{ obj: meta as Record<string, any>, path: "metadata" },
|
||||
];
|
||||
while (stack.length) {
|
||||
const { obj, path } = stack.pop()!;
|
||||
for (const key of Object.keys(obj)) {
|
||||
@@ -264,7 +284,10 @@ export async function getItemByName(itemName: string): Promise<Item | null> {
|
||||
return buildItem(item);
|
||||
}
|
||||
|
||||
export async function getItemBySource(sourceTableName: string, sourceTableId: number): Promise<Item | null> {
|
||||
export async function getItemBySource(
|
||||
sourceTableName: string,
|
||||
sourceTableId: number,
|
||||
): Promise<Item | null> {
|
||||
const item = await prisma.item.findUnique({
|
||||
where: {
|
||||
source_table_name_source_table_id: {
|
||||
@@ -279,15 +302,10 @@ export async function getItemBySource(sourceTableName: string, sourceTableId: nu
|
||||
return buildItem(item);
|
||||
}
|
||||
|
||||
export async function searchAdminItems(params: AdminSearchParams = {}): Promise<AdminItemsResult> {
|
||||
const {
|
||||
q = "",
|
||||
category,
|
||||
rarity,
|
||||
server,
|
||||
page = 1,
|
||||
pageSize = 50,
|
||||
} = params;
|
||||
export async function searchAdminItems(
|
||||
params: AdminSearchParams = {},
|
||||
): Promise<AdminItemsResult> {
|
||||
const { q = "", category, rarity, server, page = 1, pageSize = 50 } = params;
|
||||
|
||||
await ensureItemsSeeded();
|
||||
|
||||
@@ -355,7 +373,9 @@ export async function searchAdminItems(params: AdminSearchParams = {}): Promise<
|
||||
total,
|
||||
page: safePage,
|
||||
pageSize: safePageSize,
|
||||
servers: Array.from(serversSet).sort((a, b) => a.localeCompare(b, "pt-BR", { sensitivity: "base" })),
|
||||
servers: Array.from(serversSet).sort((a, b) =>
|
||||
a.localeCompare(b, "pt-BR", { sensitivity: "base" }),
|
||||
),
|
||||
categories: categoryRows
|
||||
.map((row) => row.category)
|
||||
.filter((c): c is string => Boolean(c))
|
||||
@@ -363,12 +383,16 @@ export async function searchAdminItems(params: AdminSearchParams = {}): Promise<
|
||||
};
|
||||
}
|
||||
|
||||
export async function searchItems(params: SearchItemsParams = {}): Promise<{ items: ItemWithMeta[]; total: number }> {
|
||||
export async function searchItems(
|
||||
params: SearchItemsParams = {},
|
||||
): Promise<{ items: ItemWithMeta[]; total: number }> {
|
||||
const { items, total } = await searchAdminItems(params);
|
||||
return { items, total };
|
||||
}
|
||||
|
||||
export async function createAdminItem(input: ItemUpsertInput): Promise<ItemWithMeta> {
|
||||
export async function createAdminItem(
|
||||
input: ItemUpsertInput,
|
||||
): Promise<ItemWithMeta> {
|
||||
const now = Date.now();
|
||||
// Validação: impedir qualquer campo de valor em metadata (preço etc.)
|
||||
assertNoValueFieldsInMetadata(input.metadata, "createAdminItem");
|
||||
@@ -384,19 +408,25 @@ export async function createAdminItem(input: ItemUpsertInput): Promise<ItemWithM
|
||||
weight: toDecimal(input.weight),
|
||||
slots: input.slots ?? null,
|
||||
skill: input.skill ?? null,
|
||||
metadata: buildMetadata({}, {
|
||||
servers: input.servers ?? [],
|
||||
tags: input.tags ?? [],
|
||||
description: input.description ?? null,
|
||||
metadata: input.metadata ?? {},
|
||||
}),
|
||||
metadata: buildMetadata(
|
||||
{},
|
||||
{
|
||||
servers: input.servers ?? [],
|
||||
tags: input.tags ?? [],
|
||||
description: input.description ?? null,
|
||||
metadata: input.metadata ?? {},
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
const created = await prisma.item.create({ data });
|
||||
return buildItemWithMeta(created);
|
||||
}
|
||||
|
||||
export async function updateAdminItem(itemId: number, input: ItemUpsertInput): Promise<ItemWithMeta> {
|
||||
export async function updateAdminItem(
|
||||
itemId: number,
|
||||
input: ItemUpsertInput,
|
||||
): Promise<ItemWithMeta> {
|
||||
const existing = await prisma.item.findUnique({ where: { item_id: itemId } });
|
||||
if (!existing) {
|
||||
throw new Error("Item não encontrado");
|
||||
@@ -408,24 +438,34 @@ export async function updateAdminItem(itemId: number, input: ItemUpsertInput): P
|
||||
const metadata = buildMetadata(existingMeta, {
|
||||
servers: input.servers ?? extractServers(existingMeta),
|
||||
tags: input.tags ?? extractTags(existingMeta),
|
||||
description: input.description !== undefined ? input.description : existingMeta.description,
|
||||
description:
|
||||
input.description !== undefined
|
||||
? input.description
|
||||
: existingMeta.description,
|
||||
metadata: input.metadata,
|
||||
});
|
||||
|
||||
const data: Prisma.ItemUpdateInput = {
|
||||
item_name: input.item_name ?? existing.item_name,
|
||||
category: input.category ?? existing.category,
|
||||
image_local_path: input.image_local_path !== undefined ? input.image_local_path : existing.image_local_path,
|
||||
image_url: input.image_url !== undefined ? input.image_url : existing.image_url,
|
||||
image_local_path:
|
||||
input.image_local_path !== undefined
|
||||
? input.image_local_path
|
||||
: existing.image_local_path,
|
||||
image_url:
|
||||
input.image_url !== undefined ? input.image_url : existing.image_url,
|
||||
rarity: input.rarity !== undefined ? input.rarity : existing.rarity,
|
||||
level: input.level !== undefined ? input.level : existing.level,
|
||||
weight: input.weight !== undefined ? toDecimal(input.weight) : existing.weight,
|
||||
weight:
|
||||
input.weight !== undefined ? toDecimal(input.weight) : existing.weight,
|
||||
slots: input.slots !== undefined ? input.slots : existing.slots,
|
||||
skill: input.skill !== undefined ? input.skill : existing.skill,
|
||||
metadata,
|
||||
};
|
||||
|
||||
const updated = await prisma.item.update({ where: { item_id: itemId }, data });
|
||||
const updated = await prisma.item.update({
|
||||
where: { item_id: itemId },
|
||||
data,
|
||||
});
|
||||
return buildItemWithMeta(updated);
|
||||
}
|
||||
|
||||
+188
-68
@@ -3,15 +3,20 @@
|
||||
* Migrado de lib/db/queries/listings.ts
|
||||
*/
|
||||
|
||||
import type { Prisma } from "@prisma/client";
|
||||
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 type ListingStatus =
|
||||
| "active"
|
||||
| "sold"
|
||||
| "cancelled"
|
||||
| "in_escrow"
|
||||
| "expired";
|
||||
|
||||
export interface Listing {
|
||||
listing_id: number;
|
||||
@@ -88,16 +93,19 @@ export interface ListingFilters {
|
||||
search?: string;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
sortBy?: "created_at" | "price_pix" | "price_tibia_coins" | "price_gold_coins" | "views_count";
|
||||
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> {
|
||||
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) {
|
||||
@@ -114,7 +122,9 @@ export async function createListing(
|
||||
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,
|
||||
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,
|
||||
@@ -122,9 +132,14 @@ export async function createListing(
|
||||
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,
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -141,11 +156,21 @@ export async function createListing(
|
||||
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,
|
||||
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,
|
||||
@@ -155,7 +180,9 @@ export async function createListing(
|
||||
/**
|
||||
* Busca anúncio por ID
|
||||
*/
|
||||
export async function getListingById(listingId: number): Promise<Listing | null> {
|
||||
export async function getListingById(
|
||||
listingId: number,
|
||||
): Promise<Listing | null> {
|
||||
const listing = await prisma.listing.findUnique({
|
||||
where: { listing_id: listingId },
|
||||
});
|
||||
@@ -179,11 +206,21 @@ export async function getListingById(listingId: number): Promise<Listing | null>
|
||||
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,
|
||||
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,
|
||||
@@ -194,7 +231,7 @@ export async function getListingById(listingId: number): Promise<Listing | null>
|
||||
* Lista anúncios com filtros e paginação
|
||||
*/
|
||||
export async function getListings(
|
||||
filters: ListingFilters = {}
|
||||
filters: ListingFilters = {},
|
||||
): Promise<{ listings: Listing[]; total: number }> {
|
||||
const {
|
||||
status,
|
||||
@@ -239,7 +276,13 @@ export async function getListings(
|
||||
}
|
||||
|
||||
// Validar sortBy
|
||||
const validSortColumns = ["created_at", "price_pix", "price_tibia_coins", "price_gold_coins", "views_count"];
|
||||
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",
|
||||
@@ -258,7 +301,7 @@ export async function getListings(
|
||||
|
||||
// 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}`)
|
||||
listings.map((l) => `${l.seller_id}:${l.server_name}`),
|
||||
);
|
||||
|
||||
const charactersMap = new Map<string, string>();
|
||||
@@ -277,7 +320,7 @@ export async function getListings(
|
||||
}
|
||||
|
||||
const highestOffers = await getHighestCustomOffersForListings(
|
||||
listings.map((listing) => listing.listing_id)
|
||||
listings.map((listing) => listing.listing_id),
|
||||
);
|
||||
|
||||
// Converter todos os campos Decimal e BigInt para números JavaScript
|
||||
@@ -287,11 +330,21 @@ export async function getListings(
|
||||
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,
|
||||
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,
|
||||
@@ -309,17 +362,21 @@ export async function getListings(
|
||||
*/
|
||||
export async function updateListing(
|
||||
listingId: number,
|
||||
data: UpdateListingData
|
||||
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.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.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;
|
||||
}
|
||||
@@ -338,17 +395,29 @@ export async function updateListing(
|
||||
},
|
||||
});
|
||||
|
||||
const highestOffer = await getHighestCustomOfferForListing(listing.listing_id);
|
||||
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,
|
||||
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,
|
||||
@@ -364,7 +433,7 @@ export async function deleteListing(listingId: number): Promise<boolean> {
|
||||
where: { listing_id: listingId },
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -388,7 +457,7 @@ export async function incrementViews(listingId: number): Promise<void> {
|
||||
*/
|
||||
export async function getListingsBySeller(
|
||||
sellerId: number,
|
||||
filters: { status?: ListingStatus; page?: number; pageSize?: number } = {}
|
||||
filters: { status?: ListingStatus; page?: number; pageSize?: number } = {},
|
||||
): Promise<{ listings: Listing[]; total: number }> {
|
||||
return getListings({
|
||||
seller_id: sellerId,
|
||||
@@ -402,7 +471,7 @@ export async function getListingsBySeller(
|
||||
* Lista apenas anúncios ativos (incluindo vendidos nas últimas 1 hora)
|
||||
*/
|
||||
export async function getActiveListings(
|
||||
filters: Omit<ListingFilters, "status"> = {}
|
||||
filters: Omit<ListingFilters, "status"> = {},
|
||||
): Promise<{ listings: Listing[]; total: number }> {
|
||||
const {
|
||||
server_name,
|
||||
@@ -449,8 +518,18 @@ export async function getActiveListings(
|
||||
if (search) {
|
||||
const searchCondition = {
|
||||
OR: [
|
||||
{ item_name: { contains: search, mode: "insensitive" as Prisma.QueryMode } },
|
||||
{ description: { contains: search, mode: "insensitive" as Prisma.QueryMode } },
|
||||
{
|
||||
item_name: {
|
||||
contains: search,
|
||||
mode: "insensitive" as Prisma.QueryMode,
|
||||
},
|
||||
},
|
||||
{
|
||||
description: {
|
||||
contains: search,
|
||||
mode: "insensitive" as Prisma.QueryMode,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
// Combinar com a condição OR existente
|
||||
@@ -472,7 +551,13 @@ export async function getActiveListings(
|
||||
}
|
||||
|
||||
// Validar sortBy
|
||||
const validSortColumns = ["created_at", "price_pix", "price_tibia_coins", "price_gold_coins", "views_count"];
|
||||
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",
|
||||
@@ -491,7 +576,7 @@ export async function getActiveListings(
|
||||
|
||||
// Buscar character names para cada listing
|
||||
const sellerServerPairs = new Set(
|
||||
listings.map((l) => `${l.seller_id}:${l.server_name}`)
|
||||
listings.map((l) => `${l.seller_id}:${l.server_name}`),
|
||||
);
|
||||
|
||||
const charactersMap = new Map<string, string>();
|
||||
@@ -510,7 +595,7 @@ export async function getActiveListings(
|
||||
}
|
||||
|
||||
const highestOffers = await getHighestCustomOffersForListings(
|
||||
listings.map((listing) => listing.listing_id)
|
||||
listings.map((listing) => listing.listing_id),
|
||||
);
|
||||
|
||||
// Converter todos os campos Decimal e BigInt para números JavaScript
|
||||
@@ -520,11 +605,21 @@ export async function getActiveListings(
|
||||
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,
|
||||
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,
|
||||
@@ -544,8 +639,13 @@ export async function placeBid(
|
||||
listingId: number,
|
||||
bidderId: number,
|
||||
amount: number,
|
||||
paymentMethod: "pix" | "tc" | "gp"
|
||||
): Promise<{ success: boolean; listing?: Listing; error?: string; previousBidderId?: 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
|
||||
@@ -566,16 +666,26 @@ export async function placeBid(
|
||||
}
|
||||
|
||||
if (listing.seller_id === bidderId) {
|
||||
return { success: false, error: "Você não pode dar lance no seu próprio anúncio" };
|
||||
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 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)}` };
|
||||
return {
|
||||
success: false,
|
||||
error: `O lance deve ser de pelo menos ${minBid.toFixed(2)}`,
|
||||
};
|
||||
}
|
||||
|
||||
const previousBidderId = listing.current_bidder_id;
|
||||
@@ -603,12 +713,24 @@ export async function placeBid(
|
||||
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,
|
||||
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,
|
||||
@@ -617,5 +739,3 @@ export async function placeBid(
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,13 @@
|
||||
|
||||
import { prisma } from "../app";
|
||||
|
||||
export type NotificationType = "bid" | "sale" | "purchase" | "escrow" | "dispute" | "system";
|
||||
export type NotificationType =
|
||||
| "bid"
|
||||
| "sale"
|
||||
| "purchase"
|
||||
| "escrow"
|
||||
| "dispute"
|
||||
| "system";
|
||||
|
||||
export interface Notification {
|
||||
notification_id: number;
|
||||
@@ -33,7 +39,7 @@ export interface CreateNotificationData {
|
||||
* Cria uma nova notificação
|
||||
*/
|
||||
export async function createNotification(
|
||||
data: CreateNotificationData
|
||||
data: CreateNotificationData,
|
||||
): Promise<Notification> {
|
||||
const notification = await prisma.notification.create({
|
||||
data: {
|
||||
@@ -56,7 +62,7 @@ export async function createNotification(
|
||||
*/
|
||||
export async function getUserNotifications(
|
||||
userId: number,
|
||||
filters: { is_read?: boolean; type?: NotificationType; limit?: number } = {}
|
||||
filters: { is_read?: boolean; type?: NotificationType; limit?: number } = {},
|
||||
): Promise<Notification[]> {
|
||||
const { is_read, type, limit = 50 } = filters;
|
||||
|
||||
@@ -75,7 +81,7 @@ export async function getUserNotifications(
|
||||
orderBy: { created_at: "desc" },
|
||||
take: limit,
|
||||
});
|
||||
return notifications.map(notification => ({
|
||||
return notifications.map((notification) => ({
|
||||
...notification,
|
||||
notification_type: notification.notification_type as NotificationType,
|
||||
}));
|
||||
@@ -96,7 +102,10 @@ export async function getUnreadCount(userId: number): Promise<number> {
|
||||
/**
|
||||
* Marca notificação como lida
|
||||
*/
|
||||
export async function markAsRead(notificationId: number, userId: number): Promise<boolean> {
|
||||
export async function markAsRead(
|
||||
notificationId: number,
|
||||
userId: number,
|
||||
): Promise<boolean> {
|
||||
const result = await prisma.notification.updateMany({
|
||||
where: {
|
||||
notification_id: notificationId,
|
||||
@@ -130,7 +139,10 @@ export async function markAllAsRead(userId: number): Promise<number> {
|
||||
/**
|
||||
* Remove notificação
|
||||
*/
|
||||
export async function deleteNotification(notificationId: number, userId: number): Promise<boolean> {
|
||||
export async function deleteNotification(
|
||||
notificationId: number,
|
||||
userId: number,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
await prisma.notification.delete({
|
||||
where: {
|
||||
@@ -139,8 +151,7 @@ export async function deleteNotification(notificationId: number, userId: number)
|
||||
},
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,15 @@
|
||||
* Queries de Purchase Intentions usando Prisma
|
||||
*/
|
||||
|
||||
import { prisma } from "../app";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { prisma } from "../app";
|
||||
|
||||
export type PurchaseIntentionStatus = "pending" | "accepted" | "rejected" | "expired" | "completed";
|
||||
export type PurchaseIntentionStatus =
|
||||
| "pending"
|
||||
| "accepted"
|
||||
| "rejected"
|
||||
| "expired"
|
||||
| "completed";
|
||||
export type PaymentMethod = "pix" | "tibia_coins" | "gold_coins" | "trade";
|
||||
|
||||
export interface CreatePurchaseIntentionData {
|
||||
@@ -54,7 +59,8 @@ function serializePurchaseIntention(intention: any): PurchaseIntention {
|
||||
return {
|
||||
...intention,
|
||||
custom_offer_amount:
|
||||
intention.custom_offer_amount !== null && intention.custom_offer_amount !== undefined
|
||||
intention.custom_offer_amount !== null &&
|
||||
intention.custom_offer_amount !== undefined
|
||||
? Number(intention.custom_offer_amount)
|
||||
: null,
|
||||
custom_offer_currency: intention.custom_offer_currency ?? null,
|
||||
@@ -66,7 +72,7 @@ function serializePurchaseIntention(intention: any): PurchaseIntention {
|
||||
* Cria uma nova intenção de compra
|
||||
*/
|
||||
export async function createPurchaseIntention(
|
||||
data: CreatePurchaseIntentionData
|
||||
data: CreatePurchaseIntentionData,
|
||||
): Promise<PurchaseIntention> {
|
||||
const result = await prisma.purchaseIntention.create({
|
||||
data: {
|
||||
@@ -77,12 +83,14 @@ export async function createPurchaseIntention(
|
||||
expires_at: data.expires_at || null,
|
||||
status: "pending",
|
||||
custom_offer_amount:
|
||||
data.custom_offer_amount !== undefined && data.custom_offer_amount !== null
|
||||
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
|
||||
data.custom_offer_amount !== undefined &&
|
||||
data.custom_offer_amount !== null
|
||||
? new Date()
|
||||
: null,
|
||||
},
|
||||
@@ -112,7 +120,7 @@ export async function createPurchaseIntention(
|
||||
*/
|
||||
export async function getPurchaseIntentionsByListing(
|
||||
listingId: number,
|
||||
filters: { status?: PurchaseIntentionStatus; limit?: number } = {}
|
||||
filters: { status?: PurchaseIntentionStatus; limit?: number } = {},
|
||||
): Promise<PurchaseIntention[]> {
|
||||
const { status, limit = 50 } = filters;
|
||||
|
||||
@@ -151,7 +159,7 @@ export async function getPurchaseIntentionsByListing(
|
||||
*/
|
||||
export async function getPurchaseIntentionsByBuyer(
|
||||
buyerId: number,
|
||||
filters: { status?: PurchaseIntentionStatus; limit?: number } = {}
|
||||
filters: { status?: PurchaseIntentionStatus; limit?: number } = {},
|
||||
): Promise<PurchaseIntention[]> {
|
||||
const { status, limit = 50 } = filters;
|
||||
|
||||
@@ -189,7 +197,7 @@ export async function getPurchaseIntentionsByBuyer(
|
||||
* Busca uma intenção específica por ID
|
||||
*/
|
||||
export async function getPurchaseIntentionById(
|
||||
intentionId: number
|
||||
intentionId: number,
|
||||
): Promise<PurchaseIntention | null> {
|
||||
const intention = await prisma.purchaseIntention.findUnique({
|
||||
where: { intention_id: intentionId },
|
||||
@@ -220,7 +228,7 @@ export async function getPurchaseIntentionById(
|
||||
export async function updatePurchaseIntentionStatus(
|
||||
intentionId: number,
|
||||
status: PurchaseIntentionStatus,
|
||||
sellerResponse?: string
|
||||
sellerResponse?: string,
|
||||
): Promise<PurchaseIntention> {
|
||||
const updateData: any = {
|
||||
status,
|
||||
@@ -264,7 +272,9 @@ export async function updatePurchaseIntentionStatus(
|
||||
/**
|
||||
* Conta intenções pendentes de um anúncio
|
||||
*/
|
||||
export async function getPendingIntentionsCount(listingId: number): Promise<number> {
|
||||
export async function getPendingIntentionsCount(
|
||||
listingId: number,
|
||||
): Promise<number> {
|
||||
return await prisma.purchaseIntention.count({
|
||||
where: {
|
||||
listing_id: listingId,
|
||||
@@ -278,7 +288,7 @@ export async function getPendingIntentionsCount(listingId: number): Promise<numb
|
||||
*/
|
||||
export async function hasPendingIntention(
|
||||
listingId: number,
|
||||
buyerId: number
|
||||
buyerId: number,
|
||||
): Promise<boolean> {
|
||||
const count = await prisma.purchaseIntention.count({
|
||||
where: {
|
||||
@@ -296,7 +306,7 @@ export async function hasPendingIntention(
|
||||
*/
|
||||
export async function getPendingIntentionByBuyerAndListing(
|
||||
listingId: number,
|
||||
buyerId: number
|
||||
buyerId: number,
|
||||
): Promise<PurchaseIntention | null> {
|
||||
const intention = await prisma.purchaseIntention.findFirst({
|
||||
where: {
|
||||
@@ -333,7 +343,7 @@ export async function getPendingIntentionByBuyerAndListing(
|
||||
*/
|
||||
export async function updatePurchaseIntentionPaymentMethod(
|
||||
intentionId: number,
|
||||
newPaymentMethod: PaymentMethod
|
||||
newPaymentMethod: PaymentMethod,
|
||||
): Promise<PurchaseIntention> {
|
||||
// Buscar intenção atual
|
||||
const intention = await prisma.purchaseIntention.findUnique({
|
||||
@@ -346,7 +356,9 @@ export async function updatePurchaseIntentionPaymentMethod(
|
||||
|
||||
// Verificar se pode trocar (máximo 1 troca)
|
||||
if (intention.payment_method_change_count >= 1) {
|
||||
throw new Error("Você já atingiu o limite de trocas de método de pagamento");
|
||||
throw new Error(
|
||||
"Você já atingiu o limite de trocas de método de pagamento",
|
||||
);
|
||||
}
|
||||
|
||||
// Verificar se o novo método é diferente
|
||||
@@ -388,7 +400,7 @@ export async function updatePurchaseIntentionPaymentMethod(
|
||||
*/
|
||||
export async function getPendingIntentionListingIdsForBuyer(
|
||||
buyerId: number,
|
||||
listingIds: number[]
|
||||
listingIds: number[],
|
||||
): Promise<number[]> {
|
||||
if (listingIds.length === 0) {
|
||||
return [];
|
||||
@@ -416,7 +428,7 @@ export async function getPendingIntentionListingIdsForBuyer(
|
||||
}
|
||||
|
||||
export async function getHighestCustomOfferForListing(
|
||||
listingId: number
|
||||
listingId: number,
|
||||
): Promise<{ amount: number; currency: string } | null> {
|
||||
const offer = await prisma.purchaseIntention.findFirst({
|
||||
where: {
|
||||
@@ -443,7 +455,7 @@ export async function getHighestCustomOfferForListing(
|
||||
}
|
||||
|
||||
export async function getHighestCustomOffersForListings(
|
||||
listingIds: number[]
|
||||
listingIds: number[],
|
||||
): Promise<Map<number, { amount: number; currency: string }>> {
|
||||
if (listingIds.length === 0) {
|
||||
return new Map();
|
||||
@@ -461,10 +473,7 @@ export async function getHighestCustomOffersForListings(
|
||||
custom_offer_amount: true,
|
||||
custom_offer_currency: true,
|
||||
},
|
||||
orderBy: [
|
||||
{ listing_id: "asc" },
|
||||
{ custom_offer_amount: "desc" },
|
||||
],
|
||||
orderBy: [{ listing_id: "asc" }, { custom_offer_amount: "desc" }],
|
||||
});
|
||||
|
||||
const result = new Map<number, { amount: number; currency: string }>();
|
||||
@@ -479,4 +488,3 @@ export async function getHighestCustomOffersForListings(
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -39,12 +39,19 @@ export async function getAllServers(): Promise<Server[]> {
|
||||
const servers = await prisma.server.findMany({
|
||||
orderBy: { server_name: "asc" },
|
||||
});
|
||||
return servers.map(server => ({
|
||||
return servers.map((server) => ({
|
||||
...server,
|
||||
server_type: server.server_type as "pvp" | "no-pvp" | "retro-pvp" | "hardcore-pvp" | null,
|
||||
server_type: server.server_type as
|
||||
| "pvp"
|
||||
| "no-pvp"
|
||||
| "retro-pvp"
|
||||
| "hardcore-pvp"
|
||||
| null,
|
||||
location: server.location as "NA" | "SA" | "EU" | "OC" | "BR" | null,
|
||||
total_volume: server.total_volume ? Number(server.total_volume) : 0,
|
||||
average_listing_price: server.average_listing_price ? Number(server.average_listing_price) : 0,
|
||||
average_listing_price: server.average_listing_price
|
||||
? Number(server.average_listing_price)
|
||||
: 0,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -56,29 +63,45 @@ export async function getActiveServers(): Promise<Server[]> {
|
||||
where: { is_active: true },
|
||||
orderBy: { server_name: "asc" },
|
||||
});
|
||||
return servers.map(server => ({
|
||||
return servers.map((server) => ({
|
||||
...server,
|
||||
server_type: server.server_type as "pvp" | "no-pvp" | "retro-pvp" | "hardcore-pvp" | null,
|
||||
server_type: server.server_type as
|
||||
| "pvp"
|
||||
| "no-pvp"
|
||||
| "retro-pvp"
|
||||
| "hardcore-pvp"
|
||||
| null,
|
||||
location: server.location as "NA" | "SA" | "EU" | "OC" | "BR" | null,
|
||||
total_volume: server.total_volume ? Number(server.total_volume) : 0,
|
||||
average_listing_price: server.average_listing_price ? Number(server.average_listing_price) : 0,
|
||||
average_listing_price: server.average_listing_price
|
||||
? Number(server.average_listing_price)
|
||||
: 0,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Busca servidor por nome
|
||||
*/
|
||||
export async function getServerByName(serverName: string): Promise<Server | null> {
|
||||
export async function getServerByName(
|
||||
serverName: string,
|
||||
): Promise<Server | null> {
|
||||
const server = await prisma.server.findUnique({
|
||||
where: { server_name: serverName },
|
||||
});
|
||||
if (!server) return null;
|
||||
return {
|
||||
...server,
|
||||
server_type: server.server_type as "pvp" | "no-pvp" | "retro-pvp" | "hardcore-pvp" | null,
|
||||
server_type: server.server_type as
|
||||
| "pvp"
|
||||
| "no-pvp"
|
||||
| "retro-pvp"
|
||||
| "hardcore-pvp"
|
||||
| null,
|
||||
location: server.location as "NA" | "SA" | "EU" | "OC" | "BR" | null,
|
||||
total_volume: server.total_volume ? Number(server.total_volume) : 0,
|
||||
average_listing_price: server.average_listing_price ? Number(server.average_listing_price) : 0,
|
||||
average_listing_price: server.average_listing_price
|
||||
? Number(server.average_listing_price)
|
||||
: 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -92,10 +115,17 @@ export async function getServerById(serverId: number): Promise<Server | null> {
|
||||
if (!server) return null;
|
||||
return {
|
||||
...server,
|
||||
server_type: server.server_type as "pvp" | "no-pvp" | "retro-pvp" | "hardcore-pvp" | null,
|
||||
server_type: server.server_type as
|
||||
| "pvp"
|
||||
| "no-pvp"
|
||||
| "retro-pvp"
|
||||
| "hardcore-pvp"
|
||||
| null,
|
||||
location: server.location as "NA" | "SA" | "EU" | "OC" | "BR" | null,
|
||||
total_volume: server.total_volume ? Number(server.total_volume) : 0,
|
||||
average_listing_price: server.average_listing_price ? Number(server.average_listing_price) : 0,
|
||||
average_listing_price: server.average_listing_price
|
||||
? Number(server.average_listing_price)
|
||||
: 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -124,7 +154,7 @@ export async function incrementServerStats(
|
||||
total_subscribers: number;
|
||||
total_transactions: number;
|
||||
total_volume: number;
|
||||
}>
|
||||
}>,
|
||||
): Promise<void> {
|
||||
const updateData: any = {
|
||||
last_activity_at: new Date(),
|
||||
@@ -157,17 +187,24 @@ export async function incrementServerStats(
|
||||
*/
|
||||
export async function updateServerStats(
|
||||
serverName: string,
|
||||
stats: ServerStats
|
||||
stats: ServerStats,
|
||||
): Promise<void> {
|
||||
const updateData: any = {};
|
||||
|
||||
if (stats.total_listings !== undefined) updateData.total_listings = stats.total_listings;
|
||||
if (stats.total_users !== undefined) updateData.total_users = stats.total_users;
|
||||
if (stats.total_subscribers !== undefined) updateData.total_subscribers = stats.total_subscribers;
|
||||
if (stats.total_transactions !== undefined) updateData.total_transactions = stats.total_transactions;
|
||||
if (stats.total_volume !== undefined) updateData.total_volume = stats.total_volume;
|
||||
if (stats.average_listing_price !== undefined) updateData.average_listing_price = stats.average_listing_price;
|
||||
if (stats.last_activity_at !== undefined) updateData.last_activity_at = stats.last_activity_at;
|
||||
if (stats.total_listings !== undefined)
|
||||
updateData.total_listings = stats.total_listings;
|
||||
if (stats.total_users !== undefined)
|
||||
updateData.total_users = stats.total_users;
|
||||
if (stats.total_subscribers !== undefined)
|
||||
updateData.total_subscribers = stats.total_subscribers;
|
||||
if (stats.total_transactions !== undefined)
|
||||
updateData.total_transactions = stats.total_transactions;
|
||||
if (stats.total_volume !== undefined)
|
||||
updateData.total_volume = stats.total_volume;
|
||||
if (stats.average_listing_price !== undefined)
|
||||
updateData.average_listing_price = stats.average_listing_price;
|
||||
if (stats.last_activity_at !== undefined)
|
||||
updateData.last_activity_at = stats.last_activity_at;
|
||||
|
||||
if (Object.keys(updateData).length === 0) {
|
||||
return;
|
||||
@@ -182,7 +219,9 @@ export async function updateServerStats(
|
||||
/**
|
||||
* Incrementa total_users de múltiplos servidores
|
||||
*/
|
||||
export async function incrementUsersForServers(serverNames: string[]): Promise<void> {
|
||||
export async function incrementUsersForServers(
|
||||
serverNames: string[],
|
||||
): Promise<void> {
|
||||
if (serverNames.length === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -197,4 +236,3 @@ export async function incrementUsersForServers(serverNames: string[]): Promise<v
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ const featureSelect = {
|
||||
};
|
||||
|
||||
export async function getAllSubscriptionTiers(
|
||||
includeFeatures = false
|
||||
includeFeatures = false,
|
||||
): Promise<SubscriptionTierSummary[]> {
|
||||
const tiers = await prisma.subscriptionTier.findMany({
|
||||
select: {
|
||||
@@ -93,10 +93,7 @@ export async function getAllSubscriptionTiers(
|
||||
}
|
||||
: false,
|
||||
},
|
||||
orderBy: [
|
||||
{ level: "asc" },
|
||||
{ subscription_tier_id: "asc" },
|
||||
],
|
||||
orderBy: [{ level: "asc" }, { subscription_tier_id: "asc" }],
|
||||
});
|
||||
|
||||
return tiers as SubscriptionTierSummary[];
|
||||
@@ -104,7 +101,7 @@ export async function getAllSubscriptionTiers(
|
||||
|
||||
export async function getSubscriptionTierBySlug(
|
||||
slug: string,
|
||||
includeFeatures = false
|
||||
includeFeatures = false,
|
||||
): Promise<SubscriptionTierSummary | null> {
|
||||
const tier = await prisma.subscriptionTier.findUnique({
|
||||
where: { slug },
|
||||
@@ -124,7 +121,7 @@ export async function getSubscriptionTierBySlug(
|
||||
|
||||
export async function getSubscriptionTierById(
|
||||
tierId: number,
|
||||
includeFeatures = false
|
||||
includeFeatures = false,
|
||||
): Promise<SubscriptionTierSummary | null> {
|
||||
const tier = await prisma.subscriptionTier.findUnique({
|
||||
where: { subscription_tier_id: tierId },
|
||||
@@ -152,7 +149,7 @@ export async function getDefaultSubscriptionTier(): Promise<SubscriptionTierSumm
|
||||
}
|
||||
|
||||
export async function createSubscriptionTier(
|
||||
input: CreateSubscriptionTierInput
|
||||
input: CreateSubscriptionTierInput,
|
||||
): Promise<SubscriptionTierSummary> {
|
||||
const tier = await prisma.subscriptionTier.create({
|
||||
data: {
|
||||
@@ -194,9 +191,9 @@ export async function createSubscriptionTier(
|
||||
|
||||
export async function updateSubscriptionTier(
|
||||
tierId: number,
|
||||
input: UpdateSubscriptionTierInput
|
||||
input: UpdateSubscriptionTierInput,
|
||||
): Promise<SubscriptionTierSummary> {
|
||||
const tier = await prisma.subscriptionTier.update({
|
||||
const _tier = await prisma.subscriptionTier.update({
|
||||
where: { subscription_tier_id: tierId },
|
||||
data: {
|
||||
name: input.name ?? undefined,
|
||||
@@ -269,7 +266,7 @@ export async function deleteSubscriptionTier(tierId: number): Promise<void> {
|
||||
|
||||
export async function assignUserToSubscriptionTier(
|
||||
userId: number,
|
||||
tierId: number | null
|
||||
tierId: number | null,
|
||||
): Promise<void> {
|
||||
await prisma.user.update({
|
||||
where: { user_id: userId },
|
||||
@@ -290,9 +287,10 @@ export async function assignUserToSubscriptionTier(
|
||||
});
|
||||
}
|
||||
|
||||
export async function countUsersInSubscriptionTier(tierId: number): Promise<number> {
|
||||
export async function countUsersInSubscriptionTier(
|
||||
tierId: number,
|
||||
): Promise<number> {
|
||||
return prisma.user.count({
|
||||
where: { subscription_tier_id: tierId },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
* Migrado de lib/db/queries/telemetry.ts
|
||||
*/
|
||||
|
||||
import { prisma } from "../app";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { prisma } from "../app";
|
||||
|
||||
export type TelemetryEventType =
|
||||
| "ad_view"
|
||||
@@ -102,7 +102,7 @@ export interface MarketplaceStats {
|
||||
* Registra um evento de telemetria
|
||||
*/
|
||||
export async function trackEvent(
|
||||
data: TrackEventData
|
||||
data: TrackEventData,
|
||||
): Promise<TelemetryEvent> {
|
||||
const event = await prisma.telemetryEvent.create({
|
||||
data: {
|
||||
@@ -123,28 +123,26 @@ export async function trackEvent(
|
||||
* Registra múltiplos eventos em batch
|
||||
*/
|
||||
export async function trackEventsBatch(
|
||||
events: TrackEventData[]
|
||||
events: TrackEventData[],
|
||||
): Promise<TelemetryEvent[]> {
|
||||
if (events.length === 0) return [];
|
||||
|
||||
const eventsResult = await prisma.$transaction(
|
||||
async (tx) => {
|
||||
return await Promise.all(
|
||||
events.map((event) =>
|
||||
tx.telemetryEvent.create({
|
||||
data: {
|
||||
event_type: event.event_type as TelemetryEventType,
|
||||
ad_id: event.ad_id || null,
|
||||
user_id: event.user_id || null,
|
||||
session_id: event.session_id || null,
|
||||
metadata: event.metadata || {},
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
return eventsResult.map(event => ({
|
||||
const eventsResult = await prisma.$transaction(async (tx) => {
|
||||
return await Promise.all(
|
||||
events.map((event) =>
|
||||
tx.telemetryEvent.create({
|
||||
data: {
|
||||
event_type: event.event_type as TelemetryEventType,
|
||||
ad_id: event.ad_id || null,
|
||||
user_id: event.user_id || null,
|
||||
session_id: event.session_id || null,
|
||||
metadata: event.metadata || {},
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
return eventsResult.map((event) => ({
|
||||
...event,
|
||||
event_type: event.event_type as TelemetryEventType,
|
||||
}));
|
||||
@@ -153,7 +151,9 @@ export async function trackEventsBatch(
|
||||
/**
|
||||
* Busca analytics de um anúncio específico
|
||||
*/
|
||||
export async function getAdAnalytics(adId: number): Promise<AdAnalytics | null> {
|
||||
export async function getAdAnalytics(
|
||||
adId: number,
|
||||
): Promise<AdAnalytics | null> {
|
||||
const analytics = await prisma.adAnalytics.findUnique({
|
||||
where: { ad_id: adId },
|
||||
});
|
||||
@@ -173,7 +173,7 @@ export async function getAdAnalytics(adId: number): Promise<AdAnalytics | null>
|
||||
export async function getUserBehavior(
|
||||
userId: number,
|
||||
startDate?: Date,
|
||||
endDate?: Date
|
||||
endDate?: Date,
|
||||
): Promise<UserBehavior | null> {
|
||||
// Verificar se usuário existe
|
||||
const user = await prisma.user.findUnique({
|
||||
@@ -278,7 +278,7 @@ export async function getUserBehavior(
|
||||
`;
|
||||
|
||||
// Buscar item_id dos anúncios visualizados
|
||||
const itemIds = await prisma.$queryRaw<Array<{ item_id: number }>>`
|
||||
const _itemIds = await prisma.$queryRaw<Array<{ item_id: number }>>`
|
||||
SELECT DISTINCT l.item_id
|
||||
FROM telemetry_events te
|
||||
JOIN listings l ON l.listing_id = te.ad_id
|
||||
@@ -295,7 +295,7 @@ export async function getUserBehavior(
|
||||
.filter((q): q is string => !!q),
|
||||
filters_applied: filterEvents.reduce(
|
||||
(acc, r) => ({ ...acc, ...(r.metadata as any) }),
|
||||
{} as Record<string, any>
|
||||
{} as Record<string, any>,
|
||||
),
|
||||
items_viewed: viewEvents.map((r) => ({
|
||||
item_id: r.ad_id!,
|
||||
@@ -320,11 +320,12 @@ export async function getUserBehavior(
|
||||
*/
|
||||
export async function getMarketplaceStats(
|
||||
startDate?: Date,
|
||||
endDate?: Date
|
||||
endDate?: Date,
|
||||
): Promise<MarketplaceStats> {
|
||||
const dateFilter = startDate && endDate
|
||||
? Prisma.sql`WHERE timestamp BETWEEN ${startDate} AND ${endDate}`
|
||||
: Prisma.empty;
|
||||
const dateFilter =
|
||||
startDate && endDate
|
||||
? Prisma.sql`WHERE timestamp BETWEEN ${startDate} AND ${endDate}`
|
||||
: Prisma.empty;
|
||||
|
||||
// Total de transações
|
||||
const transactionsResult = await prisma.$queryRaw<Array<{ count: bigint }>>`
|
||||
@@ -366,7 +367,8 @@ export async function getMarketplaceStats(
|
||||
|
||||
const viewsCount = Number(viewsResult[0]?.count || 0);
|
||||
const purchasesCount = Number(purchasesResult[0]?.count || 0);
|
||||
const conversion_rate = viewsCount > 0 ? (purchasesCount / viewsCount) * 100 : 0;
|
||||
const conversion_rate =
|
||||
viewsCount > 0 ? (purchasesCount / viewsCount) * 100 : 0;
|
||||
|
||||
// Itens mais procurados
|
||||
const mostSearchedResult = await prisma.$queryRaw<
|
||||
@@ -415,9 +417,7 @@ export async function getMarketplaceStats(
|
||||
`;
|
||||
|
||||
// Tempo médio para venda
|
||||
const avgTimeResult = await prisma.$queryRaw<
|
||||
Array<{ avg_hours: string }>
|
||||
>`
|
||||
const avgTimeResult = await prisma.$queryRaw<Array<{ avg_hours: string }>>`
|
||||
SELECT AVG(aa.time_to_sell)::TEXT as avg_hours
|
||||
FROM ad_analytics aa
|
||||
WHERE aa.time_to_sell IS NOT NULL
|
||||
@@ -430,7 +430,7 @@ export async function getMarketplaceStats(
|
||||
acc[r.item_name] = parseFloat(r.avg_price);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, number>
|
||||
{} as Record<string, number>,
|
||||
),
|
||||
conversion_rate,
|
||||
most_searched_items: mostSearchedResult.map((r) => ({
|
||||
@@ -458,7 +458,7 @@ export async function getMarketplaceStats(
|
||||
export async function aggregateHourlyMetrics(): Promise<void> {
|
||||
// Usar $queryRaw para queries complexas de agregação
|
||||
// Por enquanto, manter a lógica original mas adaptada para Prisma
|
||||
|
||||
|
||||
// Agregar impressões
|
||||
await prisma.$executeRaw`
|
||||
INSERT INTO ad_analytics (ad_id, impressions, last_updated)
|
||||
@@ -617,4 +617,3 @@ export async function aggregateHourlyMetrics(): Promise<void> {
|
||||
AND l.sold_at >= NOW() - INTERVAL '1 hour'
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
* Migrado de lib/db/queries/transactions.ts
|
||||
*/
|
||||
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import { prisma } from "../app";
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
export type TransactionStatus =
|
||||
| "pending"
|
||||
@@ -64,7 +64,7 @@ export interface UpdateTransactionStatusData {
|
||||
* Cria uma nova transação
|
||||
*/
|
||||
export async function createTransaction(
|
||||
data: CreateTransactionData
|
||||
data: CreateTransactionData,
|
||||
): Promise<Transaction> {
|
||||
// Calcular escrow_fee se não fornecido (2% do valor)
|
||||
const escrowFee = data.escrow_fee ?? Number((data.amount * 0.02).toFixed(2));
|
||||
@@ -87,7 +87,11 @@ export async function createTransaction(
|
||||
amount: Number(transaction.amount),
|
||||
escrow_fee: transaction.escrow_fee ? Number(transaction.escrow_fee) : null,
|
||||
transaction_status: transaction.transaction_status as TransactionStatus,
|
||||
payment_method: transaction.payment_method as "pix" | "tibia_coins" | "gold_coins" | null,
|
||||
payment_method: transaction.payment_method as
|
||||
| "pix"
|
||||
| "tibia_coins"
|
||||
| "gold_coins"
|
||||
| null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -95,7 +99,7 @@ export async function createTransaction(
|
||||
* Busca transação por ID
|
||||
*/
|
||||
export async function getTransactionById(
|
||||
transactionId: number
|
||||
transactionId: number,
|
||||
): Promise<Transaction | null> {
|
||||
const transaction = await prisma.transaction.findUnique({
|
||||
where: { transaction_id: transactionId },
|
||||
@@ -106,7 +110,11 @@ export async function getTransactionById(
|
||||
amount: Number(transaction.amount),
|
||||
escrow_fee: transaction.escrow_fee ? Number(transaction.escrow_fee) : null,
|
||||
transaction_status: transaction.transaction_status as TransactionStatus,
|
||||
payment_method: transaction.payment_method as "pix" | "tibia_coins" | "gold_coins" | null,
|
||||
payment_method: transaction.payment_method as
|
||||
| "pix"
|
||||
| "tibia_coins"
|
||||
| "gold_coins"
|
||||
| null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -115,19 +123,25 @@ export async function getTransactionById(
|
||||
*/
|
||||
export async function updateTransactionStatus(
|
||||
transactionId: number,
|
||||
data: UpdateTransactionStatusData
|
||||
data: UpdateTransactionStatusData,
|
||||
): Promise<Transaction | null> {
|
||||
const updateData: Prisma.TransactionUpdateInput = {
|
||||
transaction_status: data.transaction_status,
|
||||
};
|
||||
|
||||
if (data.deposited_at !== undefined) updateData.deposited_at = data.deposited_at;
|
||||
if (data.delivered_at !== undefined) updateData.delivered_at = data.delivered_at;
|
||||
if (data.completed_at !== undefined) updateData.completed_at = data.completed_at;
|
||||
if (data.buyer_confirmed !== undefined) updateData.buyer_confirmed = data.buyer_confirmed;
|
||||
if (data.seller_confirmed !== undefined) updateData.seller_confirmed = data.seller_confirmed;
|
||||
if (data.deposited_at !== undefined)
|
||||
updateData.deposited_at = data.deposited_at;
|
||||
if (data.delivered_at !== undefined)
|
||||
updateData.delivered_at = data.delivered_at;
|
||||
if (data.completed_at !== undefined)
|
||||
updateData.completed_at = data.completed_at;
|
||||
if (data.buyer_confirmed !== undefined)
|
||||
updateData.buyer_confirmed = data.buyer_confirmed;
|
||||
if (data.seller_confirmed !== undefined)
|
||||
updateData.seller_confirmed = data.seller_confirmed;
|
||||
if (data.buyer_notes !== undefined) updateData.buyer_notes = data.buyer_notes;
|
||||
if (data.seller_notes !== undefined) updateData.seller_notes = data.seller_notes;
|
||||
if (data.seller_notes !== undefined)
|
||||
updateData.seller_notes = data.seller_notes;
|
||||
|
||||
const transaction = await prisma.transaction.update({
|
||||
where: { transaction_id: transactionId },
|
||||
@@ -138,7 +152,11 @@ export async function updateTransactionStatus(
|
||||
amount: Number(transaction.amount),
|
||||
escrow_fee: transaction.escrow_fee ? Number(transaction.escrow_fee) : null,
|
||||
transaction_status: transaction.transaction_status as TransactionStatus,
|
||||
payment_method: transaction.payment_method as "pix" | "tibia_coins" | "gold_coins" | null,
|
||||
payment_method: transaction.payment_method as
|
||||
| "pix"
|
||||
| "tibia_coins"
|
||||
| "gold_coins"
|
||||
| null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -152,7 +170,7 @@ export async function getTransactionsByUser(
|
||||
status?: TransactionStatus;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
} = {}
|
||||
} = {},
|
||||
): Promise<{ transactions: Transaction[]; total: number }> {
|
||||
const { role = "both", status, page = 1, pageSize = 20 } = filters;
|
||||
|
||||
@@ -163,10 +181,7 @@ export async function getTransactionsByUser(
|
||||
} else if (role === "seller") {
|
||||
where.seller_id = userId;
|
||||
} else {
|
||||
where.OR = [
|
||||
{ buyer_id: userId },
|
||||
{ seller_id: userId },
|
||||
];
|
||||
where.OR = [{ buyer_id: userId }, { seller_id: userId }];
|
||||
}
|
||||
|
||||
if (status) {
|
||||
@@ -185,14 +200,19 @@ export async function getTransactionsByUser(
|
||||
});
|
||||
|
||||
return {
|
||||
transactions: transactions.map(transaction => ({
|
||||
transactions: transactions.map((transaction) => ({
|
||||
...transaction,
|
||||
amount: Number(transaction.amount),
|
||||
escrow_fee: transaction.escrow_fee ? Number(transaction.escrow_fee) : null,
|
||||
escrow_fee: transaction.escrow_fee
|
||||
? Number(transaction.escrow_fee)
|
||||
: null,
|
||||
transaction_status: transaction.transaction_status as TransactionStatus,
|
||||
payment_method: transaction.payment_method as "pix" | "tibia_coins" | "gold_coins" | null,
|
||||
payment_method: transaction.payment_method as
|
||||
| "pix"
|
||||
| "tibia_coins"
|
||||
| "gold_coins"
|
||||
| null,
|
||||
})),
|
||||
total,
|
||||
};
|
||||
}
|
||||
|
||||
+14
-11
@@ -3,9 +3,9 @@
|
||||
* Migrado de lib/db/queries/users.ts
|
||||
*/
|
||||
|
||||
import type { User } from "@prisma/client";
|
||||
import { prisma } from "../app";
|
||||
import { getDefaultSubscriptionTier } from "./subscription-tiers";
|
||||
import type { User } from "@prisma/client";
|
||||
|
||||
export interface CreateUserData {
|
||||
username: string;
|
||||
@@ -29,7 +29,7 @@ export interface UpdateUserData {
|
||||
* Cria um novo usuário
|
||||
*/
|
||||
export async function createUser(
|
||||
data: CreateUserData
|
||||
data: CreateUserData,
|
||||
): Promise<{ userId: number; username: string; email: string }> {
|
||||
const defaultTier = await getDefaultSubscriptionTier();
|
||||
const user = await prisma.user.create({
|
||||
@@ -69,7 +69,9 @@ type UserWithTier = User & {
|
||||
} | null;
|
||||
};
|
||||
|
||||
export async function getUserByEmail(email: string): Promise<UserWithTier | null> {
|
||||
export async function getUserByEmail(
|
||||
email: string,
|
||||
): Promise<UserWithTier | null> {
|
||||
return (await prisma.user.findUnique({
|
||||
where: { email },
|
||||
include: {
|
||||
@@ -89,7 +91,7 @@ export async function getUserByEmail(email: string): Promise<UserWithTier | null
|
||||
* Busca usuário por username
|
||||
*/
|
||||
export async function getUserByUsername(
|
||||
username: string
|
||||
username: string,
|
||||
): Promise<UserWithTier | null> {
|
||||
return (await prisma.user.findUnique({
|
||||
where: { username },
|
||||
@@ -134,7 +136,7 @@ type PublicUser = {
|
||||
};
|
||||
|
||||
export async function getUserById(userId: number): Promise<PublicUser | null> {
|
||||
return await prisma.user.findUnique({
|
||||
return (await prisma.user.findUnique({
|
||||
where: { user_id: userId },
|
||||
select: {
|
||||
user_id: true,
|
||||
@@ -161,21 +163,23 @@ export async function getUserById(userId: number): Promise<PublicUser | null> {
|
||||
},
|
||||
},
|
||||
},
|
||||
}) as PublicUser | null;
|
||||
})) as PublicUser | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Busca usuário por CPF (verifica duplicidade)
|
||||
*/
|
||||
export async function getUserByCpf(cpf: string): Promise<Pick<User, 'user_id' | 'username' | 'email'> | null> {
|
||||
return await prisma.user.findUnique({
|
||||
export async function getUserByCpf(
|
||||
cpf: string,
|
||||
): Promise<Pick<User, "user_id" | "username" | "email"> | null> {
|
||||
return (await prisma.user.findUnique({
|
||||
where: { cpf },
|
||||
select: {
|
||||
user_id: true,
|
||||
username: true,
|
||||
email: true,
|
||||
},
|
||||
}) as Pick<User, 'user_id' | 'username' | 'email'> | null;
|
||||
})) as Pick<User, "user_id" | "username" | "email"> | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -183,7 +187,7 @@ export async function getUserByCpf(cpf: string): Promise<Pick<User, 'user_id' |
|
||||
*/
|
||||
export async function updateUser(
|
||||
userId: number,
|
||||
data: UpdateUserData
|
||||
data: UpdateUserData,
|
||||
): Promise<PublicUser> {
|
||||
const updateData: any = {};
|
||||
if (data.first_name !== undefined) updateData.first_name = data.first_name;
|
||||
@@ -243,4 +247,3 @@ export async function updateLastLogin(userId: number): Promise<void> {
|
||||
data: { last_login: new Date() },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export function QueryProvider({ children }: { children: React.ReactNode }) {
|
||||
retry: 1, // Tentar apenas 1 vez em caso de erro
|
||||
},
|
||||
},
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -28,4 +28,3 @@ export function QueryProvider({ children }: { children: React.ReactNode }) {
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
+17
-14
@@ -54,21 +54,24 @@ const RATE_LIMITS: Record<string, RateLimitConfig> = {
|
||||
};
|
||||
|
||||
// Limpar cache expirado a cada 5 minutos
|
||||
setInterval(() => {
|
||||
const now = Date.now();
|
||||
let cleaned = 0;
|
||||
setInterval(
|
||||
() => {
|
||||
const now = Date.now();
|
||||
let cleaned = 0;
|
||||
|
||||
for (const [key, entry] of requestCache.entries()) {
|
||||
if (now > entry.resetAt) {
|
||||
requestCache.delete(key);
|
||||
cleaned++;
|
||||
for (const [key, entry] of requestCache.entries()) {
|
||||
if (now > entry.resetAt) {
|
||||
requestCache.delete(key);
|
||||
cleaned++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cleaned > 0) {
|
||||
console.log(`🧹 Rate limiter: ${cleaned} entradas expiradas removidas`);
|
||||
}
|
||||
}, 5 * 60 * 1000);
|
||||
if (cleaned > 0) {
|
||||
console.log(`🧹 Rate limiter: ${cleaned} entradas expiradas removidas`);
|
||||
}
|
||||
},
|
||||
5 * 60 * 1000,
|
||||
);
|
||||
|
||||
/**
|
||||
* Verifica se uma requisição excede o rate limit
|
||||
@@ -78,7 +81,7 @@ setInterval(() => {
|
||||
*/
|
||||
export function checkRateLimit(
|
||||
ip: string,
|
||||
path: string
|
||||
path: string,
|
||||
): {
|
||||
allowed: boolean;
|
||||
remaining: number;
|
||||
@@ -180,6 +183,6 @@ export function createRateLimitResponse(resetAt: number): Response {
|
||||
"X-RateLimit-Remaining": "0",
|
||||
"X-RateLimit-Reset": new Date(resetAt).toISOString(),
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
+3
-4
@@ -3,7 +3,7 @@
|
||||
* Gerencia conexão e fornece helpers para cache e sessões
|
||||
*/
|
||||
|
||||
import { createClient, RedisClientType } from "redis";
|
||||
import { createClient, type RedisClientType } from "redis";
|
||||
|
||||
let client: RedisClientType | null = null;
|
||||
|
||||
@@ -13,7 +13,7 @@ let client: RedisClientType | null = null;
|
||||
function validateEnvVars() {
|
||||
if (!process.env.REDIS_DB) {
|
||||
throw new Error(
|
||||
"REDIS_DB não está definido. Configure a variável de ambiente REDIS_DB com a connection string do Redis."
|
||||
"REDIS_DB não está definido. Configure a variável de ambiente REDIS_DB com a connection string do Redis.",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -74,7 +74,7 @@ export async function isRedisConnected(): Promise<boolean> {
|
||||
* Fecha conexão com Redis
|
||||
*/
|
||||
export async function closeRedis(): Promise<void> {
|
||||
if (client && client.isOpen) {
|
||||
if (client?.isOpen) {
|
||||
await client.quit();
|
||||
client = null;
|
||||
console.log("🔌 Conexão Redis fechada");
|
||||
@@ -93,4 +93,3 @@ if (typeof process !== "undefined") {
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -31,7 +31,10 @@ export interface SessionData {
|
||||
/**
|
||||
* Cria ou atualiza uma sessão
|
||||
*/
|
||||
export async function setSession(sessionId: string, data: SessionData): Promise<void> {
|
||||
export async function setSession(
|
||||
sessionId: string,
|
||||
data: SessionData,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const client = getRedisClient();
|
||||
if (!client.isOpen) {
|
||||
@@ -54,7 +57,9 @@ export async function setSession(sessionId: string, data: SessionData): Promise<
|
||||
/**
|
||||
* Obtém dados de uma sessão
|
||||
*/
|
||||
export async function getSession(sessionId: string): Promise<SessionData | null> {
|
||||
export async function getSession(
|
||||
sessionId: string,
|
||||
): Promise<SessionData | null> {
|
||||
try {
|
||||
const client = getRedisClient();
|
||||
if (!client.isOpen) {
|
||||
@@ -146,4 +151,3 @@ export async function refreshSession(sessionId: string): Promise<void> {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ interface MaskOptions {
|
||||
removeSensitive?: boolean;
|
||||
}
|
||||
|
||||
const isProduction = process.env.NODE_ENV === "production";
|
||||
const _isProduction = process.env.NODE_ENV === "production";
|
||||
|
||||
/**
|
||||
* Mascara email mantendo primeiro caractere e domínio
|
||||
@@ -38,7 +38,7 @@ function maskEmail(email: string): string {
|
||||
if (!email || !email.includes("@")) return email;
|
||||
|
||||
const [localPart, domain] = email.split("@");
|
||||
const maskedLocal = localPart.substring(0, 1) + "***";
|
||||
const maskedLocal = `${localPart.substring(0, 1)}***`;
|
||||
|
||||
return `${maskedLocal}@${domain}`;
|
||||
}
|
||||
|
||||
+96
-87
@@ -3,13 +3,13 @@
|
||||
* Integrado com Next.js
|
||||
*/
|
||||
|
||||
import type { Server as HTTPServer } from "node:http";
|
||||
import { Server as SocketIOServer } from "socket.io";
|
||||
import type { Server as HTTPServer } from "http";
|
||||
import { verifyAccessToken } from "@/lib/auth/jwt";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { Message } from "@/lib/mongodb/models/Message";
|
||||
import { Chat } from "@/lib/mongodb/models/Chat";
|
||||
import { connectMongoDB } from "@/lib/mongodb/connection";
|
||||
import { Chat } from "@/lib/mongodb/models/Chat";
|
||||
import { Message } from "@/lib/mongodb/models/Message";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
|
||||
let io: SocketIOServer | null = null;
|
||||
|
||||
@@ -40,7 +40,7 @@ export function initSocketIO(httpServer?: HTTPServer): SocketIOServer {
|
||||
try {
|
||||
// Tentar obter token do auth primeiro
|
||||
const token = socket.handshake.auth.token;
|
||||
|
||||
|
||||
if (token) {
|
||||
const payload = verifyAccessToken(token);
|
||||
if (payload) {
|
||||
@@ -58,7 +58,7 @@ export function initSocketIO(httpServer?: HTTPServer): SocketIOServer {
|
||||
const sessionId = sessionIdMatch[1];
|
||||
const { getSession } = await import("@/lib/redis/session");
|
||||
const session = await getSession(sessionId);
|
||||
|
||||
|
||||
if (session) {
|
||||
const payload = verifyAccessToken(session.accessToken);
|
||||
if (payload) {
|
||||
@@ -90,7 +90,7 @@ export function initSocketIO(httpServer?: HTTPServer): SocketIOServer {
|
||||
socket.on("join_chats", async () => {
|
||||
try {
|
||||
await connectMongoDB();
|
||||
|
||||
|
||||
// Buscar todos os chats do usuário
|
||||
const chats = await Chat.find({
|
||||
$or: [{ buyer_id: userId }, { seller_id: userId }],
|
||||
@@ -125,7 +125,9 @@ export function initSocketIO(httpServer?: HTTPServer): SocketIOServer {
|
||||
}
|
||||
|
||||
if (chat.buyer_id !== userId && chat.seller_id !== userId) {
|
||||
socket.emit("error", { message: "Você não tem permissão para acessar este chat" });
|
||||
socket.emit("error", {
|
||||
message: "Você não tem permissão para acessar este chat",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -140,7 +142,7 @@ export function initSocketIO(httpServer?: HTTPServer): SocketIOServer {
|
||||
},
|
||||
{
|
||||
$set: { read_at: new Date() },
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Atualizar contador de não lidas
|
||||
@@ -162,85 +164,93 @@ export function initSocketIO(httpServer?: HTTPServer): SocketIOServer {
|
||||
});
|
||||
|
||||
// Enviar mensagem
|
||||
socket.on("send_message", async (data: { chat_id: string; content: string }) => {
|
||||
try {
|
||||
const { chat_id, content } = data;
|
||||
socket.on(
|
||||
"send_message",
|
||||
async (data: { chat_id: string; content: string }) => {
|
||||
try {
|
||||
const { chat_id, content } = data;
|
||||
|
||||
if (!content || content.trim().length === 0) {
|
||||
socket.emit("error", { message: "Mensagem não pode estar vazia" });
|
||||
return;
|
||||
if (!content || content.trim().length === 0) {
|
||||
socket.emit("error", { message: "Mensagem não pode estar vazia" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (content.length > 1000) {
|
||||
socket.emit("error", {
|
||||
message: "Mensagem muito longa (máximo 1000 caracteres)",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await connectMongoDB();
|
||||
|
||||
// Verificar se o chat existe e o usuário pertence a ele
|
||||
const chat = await Chat.findOne({ chat_id });
|
||||
|
||||
if (!chat) {
|
||||
socket.emit("error", { message: "Chat não encontrado" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (chat.buyer_id !== userId && chat.seller_id !== userId) {
|
||||
socket.emit("error", {
|
||||
message:
|
||||
"Você não tem permissão para enviar mensagens neste chat",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Criar mensagem
|
||||
const message = new Message({
|
||||
chat_id,
|
||||
sender_id: userId,
|
||||
content: content.trim(),
|
||||
message_type: "text",
|
||||
});
|
||||
|
||||
await message.save();
|
||||
|
||||
// Atualizar chat
|
||||
chat.last_message_at = new Date();
|
||||
chat.last_message_preview = content.substring(0, 100);
|
||||
|
||||
// Atualizar contador de não lidas
|
||||
if (chat.buyer_id === userId) {
|
||||
chat.seller_unread_count += 1;
|
||||
} else {
|
||||
chat.buyer_unread_count += 1;
|
||||
}
|
||||
|
||||
await chat.save();
|
||||
|
||||
// Enviar mensagem para todos no chat (exceto o remetente)
|
||||
io?.to(`chat:${chat_id}`).emit("new_message", {
|
||||
message: {
|
||||
_id: message._id.toString(),
|
||||
chat_id: message.chat_id,
|
||||
sender_id: message.sender_id,
|
||||
content: message.content,
|
||||
created_at: message.created_at,
|
||||
read_at: message.read_at,
|
||||
message_type: message.message_type,
|
||||
},
|
||||
});
|
||||
|
||||
// Confirmar para o remetente
|
||||
socket.emit("message_sent", {
|
||||
message_id: message._id.toString(),
|
||||
});
|
||||
|
||||
// Mensagem enviada (log desabilitado)
|
||||
} catch (error) {
|
||||
secureLogger.error("Erro ao enviar mensagem", {
|
||||
userId,
|
||||
error: error instanceof Error ? error.message : "Erro desconhecido",
|
||||
});
|
||||
socket.emit("error", { message: "Erro ao enviar mensagem" });
|
||||
}
|
||||
|
||||
if (content.length > 1000) {
|
||||
socket.emit("error", { message: "Mensagem muito longa (máximo 1000 caracteres)" });
|
||||
return;
|
||||
}
|
||||
|
||||
await connectMongoDB();
|
||||
|
||||
// Verificar se o chat existe e o usuário pertence a ele
|
||||
const chat = await Chat.findOne({ chat_id });
|
||||
|
||||
if (!chat) {
|
||||
socket.emit("error", { message: "Chat não encontrado" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (chat.buyer_id !== userId && chat.seller_id !== userId) {
|
||||
socket.emit("error", { message: "Você não tem permissão para enviar mensagens neste chat" });
|
||||
return;
|
||||
}
|
||||
|
||||
// Criar mensagem
|
||||
const message = new Message({
|
||||
chat_id,
|
||||
sender_id: userId,
|
||||
content: content.trim(),
|
||||
message_type: "text",
|
||||
});
|
||||
|
||||
await message.save();
|
||||
|
||||
// Atualizar chat
|
||||
chat.last_message_at = new Date();
|
||||
chat.last_message_preview = content.substring(0, 100);
|
||||
|
||||
// Atualizar contador de não lidas
|
||||
if (chat.buyer_id === userId) {
|
||||
chat.seller_unread_count += 1;
|
||||
} else {
|
||||
chat.buyer_unread_count += 1;
|
||||
}
|
||||
|
||||
await chat.save();
|
||||
|
||||
// Enviar mensagem para todos no chat (exceto o remetente)
|
||||
io!.to(`chat:${chat_id}`).emit("new_message", {
|
||||
message: {
|
||||
_id: message._id.toString(),
|
||||
chat_id: message.chat_id,
|
||||
sender_id: message.sender_id,
|
||||
content: message.content,
|
||||
created_at: message.created_at,
|
||||
read_at: message.read_at,
|
||||
message_type: message.message_type,
|
||||
},
|
||||
});
|
||||
|
||||
// Confirmar para o remetente
|
||||
socket.emit("message_sent", {
|
||||
message_id: message._id.toString(),
|
||||
});
|
||||
|
||||
// Mensagem enviada (log desabilitado)
|
||||
} catch (error) {
|
||||
secureLogger.error("Erro ao enviar mensagem", {
|
||||
userId,
|
||||
error: error instanceof Error ? error.message : "Erro desconhecido",
|
||||
});
|
||||
socket.emit("error", { message: "Erro ao enviar mensagem" });
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// Indicador de "digitando..."
|
||||
socket.on("typing", (data: { chat_id: string; is_typing: boolean }) => {
|
||||
@@ -267,4 +277,3 @@ export function initSocketIO(httpServer?: HTTPServer): SocketIOServer {
|
||||
export function getSocketIO(): SocketIOServer | null {
|
||||
return io;
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ interface PendingEvent extends TelemetryEvent {
|
||||
|
||||
export class MarketTelemetry {
|
||||
private sessionId: string;
|
||||
private userId?: number;
|
||||
private eventQueue: PendingEvent[] = [];
|
||||
private viewStartTimes: Map<number, number> = new Map();
|
||||
private batchInterval: NodeJS.Timeout | null = null;
|
||||
@@ -53,7 +52,7 @@ export class MarketTelemetry {
|
||||
*/
|
||||
private generateSessionId(): string {
|
||||
if (typeof window === "undefined") return "";
|
||||
|
||||
|
||||
// Tentar recuperar da sessionStorage
|
||||
let sessionId = sessionStorage.getItem("telemetry_session_id");
|
||||
if (sessionId) return sessionId;
|
||||
@@ -207,7 +206,7 @@ export class MarketTelemetry {
|
||||
trackCustomEvent(
|
||||
eventType: TelemetryEventType | string,
|
||||
adId?: number,
|
||||
metadata?: Record<string, any>
|
||||
metadata?: Record<string, any>,
|
||||
): void {
|
||||
this.queueEvent({
|
||||
event_type: eventType as TelemetryEventType,
|
||||
@@ -237,40 +236,40 @@ export class MarketTelemetry {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Envia eventos em batch para o servidor
|
||||
*/
|
||||
async flushQueue(): Promise<void> {
|
||||
if (this.eventQueue.length === 0) return;
|
||||
/**
|
||||
* Envia eventos em batch para o servidor
|
||||
*/
|
||||
async flushQueue(): Promise<void> {
|
||||
if (this.eventQueue.length === 0) return;
|
||||
|
||||
const eventsToSend = [...this.eventQueue];
|
||||
this.eventQueue = [];
|
||||
const eventsToSend = [...this.eventQueue];
|
||||
this.eventQueue = [];
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/telemetry", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
events: eventsToSend.map(({ timestamp, ...event }) => ({
|
||||
...event,
|
||||
session_id: this.sessionId,
|
||||
})),
|
||||
}),
|
||||
});
|
||||
try {
|
||||
const response = await fetch("/api/telemetry", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
events: eventsToSend.map(({ timestamp, ...event }) => ({
|
||||
...event,
|
||||
session_id: this.sessionId,
|
||||
})),
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
// Re-adicionar eventos à fila se falhar (mas não logar erro para não poluir console)
|
||||
this.eventQueue.unshift(...eventsToSend);
|
||||
// Silenciar erro para não gerar mensagens no console
|
||||
}
|
||||
} catch (error) {
|
||||
// Re-adicionar eventos à fila se falhar (mas não logar erro)
|
||||
this.eventQueue.unshift(...eventsToSend);
|
||||
// Silenciar erro para não gerar mensagens no console
|
||||
}
|
||||
if (!response.ok) {
|
||||
// Re-adicionar eventos à fila se falhar (mas não logar erro para não poluir console)
|
||||
this.eventQueue.unshift(...eventsToSend);
|
||||
// Silenciar erro para não gerar mensagens no console
|
||||
}
|
||||
} catch (_error) {
|
||||
// Re-adicionar eventos à fila se falhar (mas não logar erro)
|
||||
this.eventQueue.unshift(...eventsToSend);
|
||||
// Silenciar erro para não gerar mensagens no console
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inicia intervalo de envio automático
|
||||
@@ -306,24 +305,25 @@ export class MarketTelemetry {
|
||||
if (this.eventQueue.length === 0) return;
|
||||
|
||||
try {
|
||||
const eventsToSend = this.eventQueue.map(({ timestamp, ...event }) => event);
|
||||
|
||||
const eventsToSend = this.eventQueue.map(
|
||||
({ timestamp, ...event }) => event,
|
||||
);
|
||||
|
||||
// Usar sendBeacon para garantir envio
|
||||
const eventsWithSession = eventsToSend.map((event) => ({
|
||||
...event,
|
||||
session_id: this.sessionId,
|
||||
}));
|
||||
|
||||
|
||||
// Usar Blob para evitar problemas de message channel
|
||||
const blob = new Blob(
|
||||
[JSON.stringify({ events: eventsWithSession })],
|
||||
{ type: "application/json" }
|
||||
);
|
||||
|
||||
const blob = new Blob([JSON.stringify({ events: eventsWithSession })], {
|
||||
type: "application/json",
|
||||
});
|
||||
|
||||
if (navigator.sendBeacon) {
|
||||
navigator.sendBeacon("/api/telemetry", blob);
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
// Silenciar erro para não gerar mensagens no console
|
||||
}
|
||||
});
|
||||
@@ -358,4 +358,3 @@ export function getTelemetry(userId?: number): MarketTelemetry {
|
||||
}
|
||||
return telemetryInstance;
|
||||
}
|
||||
|
||||
+7
-6
@@ -1,8 +1,8 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10,10 +10,11 @@ export function cn(...inputs: ClassValue[]) {
|
||||
*/
|
||||
export function formatTimeAgo(date: Date | string): string {
|
||||
const dateObj = typeof date === "string" ? new Date(date) : date;
|
||||
const seconds = Math.floor((new Date().getTime() - dateObj.getTime()) / 1000);
|
||||
const seconds = Math.floor((Date.now() - dateObj.getTime()) / 1000);
|
||||
if (seconds < 60) return "há alguns segundos";
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
if (minutes < 60) return `há ${minutes} ${minutes === 1 ? "minuto" : "minutos"}`;
|
||||
if (minutes < 60)
|
||||
return `há ${minutes} ${minutes === 1 ? "minuto" : "minutos"}`;
|
||||
const hours = Math.floor(minutes / 60);
|
||||
if (hours < 24) return `há ${hours} ${hours === 1 ? "hora" : "horas"}`;
|
||||
const days = Math.floor(hours / 24);
|
||||
@@ -22,4 +23,4 @@ export function formatTimeAgo(date: Date | string): string {
|
||||
if (months < 12) return `há ${months} ${months === 1 ? "mês" : "meses"}`;
|
||||
const years = Math.floor(months / 12);
|
||||
return `há ${years} ${years === 1 ? "ano" : "anos"}`;
|
||||
}
|
||||
}
|
||||
@@ -71,4 +71,3 @@ export function getCountryByCode(code: string): Country | undefined {
|
||||
export function getCountryByDialCode(dialCode: string): Country | undefined {
|
||||
return COUNTRIES.find((c) => c.dialCode === dialCode);
|
||||
}
|
||||
|
||||
+46
-29
@@ -8,19 +8,19 @@
|
||||
*/
|
||||
export function formatPhone(value: string): string {
|
||||
const cleaned = value.replace(/\D/g, "");
|
||||
|
||||
|
||||
if (cleaned.length <= 2) {
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
|
||||
if (cleaned.length <= 6) {
|
||||
return `(${cleaned.slice(0, 2)}) ${cleaned.slice(2)}`;
|
||||
}
|
||||
|
||||
|
||||
if (cleaned.length <= 10) {
|
||||
return `(${cleaned.slice(0, 2)}) ${cleaned.slice(2, 6)}-${cleaned.slice(6)}`;
|
||||
}
|
||||
|
||||
|
||||
// Telefone com 11 dígitos (celular)
|
||||
return `(${cleaned.slice(0, 2)}) ${cleaned.slice(2, 7)}-${cleaned.slice(7, 11)}`;
|
||||
}
|
||||
@@ -30,34 +30,42 @@ export function formatPhone(value: string): string {
|
||||
* @param phoneNumber - Número do telefone (apenas dígitos, sem código de país)
|
||||
* @param countryCode - Código ISO do país (ex: "BR", "US", "GB")
|
||||
*/
|
||||
function formatPhoneByCountry(phoneNumber: string, countryCode: string): string {
|
||||
function formatPhoneByCountry(
|
||||
phoneNumber: string,
|
||||
countryCode: string,
|
||||
): string {
|
||||
const len = phoneNumber.length;
|
||||
|
||||
switch (countryCode) {
|
||||
// Brasil: (XX) XXXXX-XXXX ou (XX) XXXX-XXXX
|
||||
case "BR":
|
||||
if (len <= 2) return phoneNumber;
|
||||
if (len <= 6) return `(${phoneNumber.slice(0, 2)}) ${phoneNumber.slice(2)}`;
|
||||
if (len <= 10) return `(${phoneNumber.slice(0, 2)}) ${phoneNumber.slice(2, 6)}-${phoneNumber.slice(6)}`;
|
||||
if (len <= 6)
|
||||
return `(${phoneNumber.slice(0, 2)}) ${phoneNumber.slice(2)}`;
|
||||
if (len <= 10)
|
||||
return `(${phoneNumber.slice(0, 2)}) ${phoneNumber.slice(2, 6)}-${phoneNumber.slice(6)}`;
|
||||
return `(${phoneNumber.slice(0, 2)}) ${phoneNumber.slice(2, 7)}-${phoneNumber.slice(7, 11)}`;
|
||||
|
||||
// EUA/Canadá: (XXX) XXX-XXXX
|
||||
case "US":
|
||||
case "CA":
|
||||
if (len <= 3) return phoneNumber;
|
||||
if (len <= 6) return `(${phoneNumber.slice(0, 3)}) ${phoneNumber.slice(3)}`;
|
||||
if (len <= 6)
|
||||
return `(${phoneNumber.slice(0, 3)}) ${phoneNumber.slice(3)}`;
|
||||
return `(${phoneNumber.slice(0, 3)}) ${phoneNumber.slice(3, 6)}-${phoneNumber.slice(6, 10)}`;
|
||||
|
||||
// México: (XX) XXXX XXXX
|
||||
case "MX":
|
||||
if (len <= 2) return phoneNumber;
|
||||
if (len <= 6) return `(${phoneNumber.slice(0, 2)}) ${phoneNumber.slice(2)}`;
|
||||
if (len <= 6)
|
||||
return `(${phoneNumber.slice(0, 2)}) ${phoneNumber.slice(2)}`;
|
||||
return `(${phoneNumber.slice(0, 2)}) ${phoneNumber.slice(2, 6)} ${phoneNumber.slice(6, 10)}`;
|
||||
|
||||
// Argentina: (XX) XXXX-XXXX
|
||||
case "AR":
|
||||
if (len <= 2) return phoneNumber;
|
||||
if (len <= 6) return `(${phoneNumber.slice(0, 2)}) ${phoneNumber.slice(2)}`;
|
||||
if (len <= 6)
|
||||
return `(${phoneNumber.slice(0, 2)}) ${phoneNumber.slice(2)}`;
|
||||
return `(${phoneNumber.slice(0, 2)}) ${phoneNumber.slice(2, 6)}-${phoneNumber.slice(6, 10)}`;
|
||||
|
||||
// Chile: X XXXX XXXX
|
||||
@@ -130,14 +138,16 @@ function formatPhoneByCountry(phoneNumber: string, countryCode: string): string
|
||||
case "BE":
|
||||
if (len <= 4) return phoneNumber;
|
||||
if (len <= 6) return `${phoneNumber.slice(0, 4)} ${phoneNumber.slice(4)}`;
|
||||
if (len <= 8) return `${phoneNumber.slice(0, 4)} ${phoneNumber.slice(4, 6)} ${phoneNumber.slice(6)}`;
|
||||
if (len <= 8)
|
||||
return `${phoneNumber.slice(0, 4)} ${phoneNumber.slice(4, 6)} ${phoneNumber.slice(6)}`;
|
||||
return `${phoneNumber.slice(0, 4)} ${phoneNumber.slice(4, 6)} ${phoneNumber.slice(6, 8)} ${phoneNumber.slice(8)}`;
|
||||
|
||||
// Suíça: XX XXX XX XX
|
||||
case "CH":
|
||||
if (len <= 2) return phoneNumber;
|
||||
if (len <= 5) return `${phoneNumber.slice(0, 2)} ${phoneNumber.slice(2)}`;
|
||||
if (len <= 7) return `${phoneNumber.slice(0, 2)} ${phoneNumber.slice(2, 5)} ${phoneNumber.slice(5)}`;
|
||||
if (len <= 7)
|
||||
return `${phoneNumber.slice(0, 2)} ${phoneNumber.slice(2, 5)} ${phoneNumber.slice(5)}`;
|
||||
return `${phoneNumber.slice(0, 2)} ${phoneNumber.slice(2, 5)} ${phoneNumber.slice(5, 7)} ${phoneNumber.slice(7)}`;
|
||||
|
||||
// Áustria: XXXX XXXX
|
||||
@@ -155,7 +165,8 @@ function formatPhoneByCountry(phoneNumber: string, countryCode: string): string
|
||||
case "RU":
|
||||
if (len <= 3) return phoneNumber;
|
||||
if (len <= 6) return `${phoneNumber.slice(0, 3)} ${phoneNumber.slice(3)}`;
|
||||
if (len <= 8) return `${phoneNumber.slice(0, 3)} ${phoneNumber.slice(3, 6)}-${phoneNumber.slice(6)}`;
|
||||
if (len <= 8)
|
||||
return `${phoneNumber.slice(0, 3)} ${phoneNumber.slice(3, 6)}-${phoneNumber.slice(6)}`;
|
||||
return `${phoneNumber.slice(0, 3)} ${phoneNumber.slice(3, 6)}-${phoneNumber.slice(6, 8)}-${phoneNumber.slice(8)}`;
|
||||
|
||||
// China: XXX XXXX XXXX
|
||||
@@ -204,7 +215,8 @@ function formatPhoneByCountry(phoneNumber: string, countryCode: string): string
|
||||
case "TR":
|
||||
if (len <= 3) return phoneNumber;
|
||||
if (len <= 6) return `${phoneNumber.slice(0, 3)} ${phoneNumber.slice(3)}`;
|
||||
if (len <= 8) return `${phoneNumber.slice(0, 3)} ${phoneNumber.slice(3, 6)} ${phoneNumber.slice(6)}`;
|
||||
if (len <= 8)
|
||||
return `${phoneNumber.slice(0, 3)} ${phoneNumber.slice(3, 6)} ${phoneNumber.slice(6)}`;
|
||||
return `${phoneNumber.slice(0, 3)} ${phoneNumber.slice(3, 6)} ${phoneNumber.slice(6, 8)} ${phoneNumber.slice(8)}`;
|
||||
|
||||
// Arábia Saudita: XX XXX XXXX
|
||||
@@ -262,21 +274,22 @@ function formatPhoneByCountry(phoneNumber: string, countryCode: string): string
|
||||
default: {
|
||||
if (len <= 4) return phoneNumber;
|
||||
if (len <= 7) return `${phoneNumber.slice(0, 3)} ${phoneNumber.slice(3)}`;
|
||||
if (len <= 10) return `${phoneNumber.slice(0, 3)} ${phoneNumber.slice(3, 6)} ${phoneNumber.slice(6)}`;
|
||||
|
||||
if (len <= 10)
|
||||
return `${phoneNumber.slice(0, 3)} ${phoneNumber.slice(3, 6)} ${phoneNumber.slice(6)}`;
|
||||
|
||||
// Para números maiores, agrupa em grupos de 3-4 dígitos
|
||||
const groups: string[] = [];
|
||||
let remaining = phoneNumber;
|
||||
|
||||
|
||||
while (remaining.length > 4) {
|
||||
groups.push(remaining.slice(0, 3));
|
||||
remaining = remaining.slice(3);
|
||||
}
|
||||
|
||||
|
||||
if (remaining.length > 0) {
|
||||
groups.push(remaining);
|
||||
}
|
||||
|
||||
|
||||
return groups.join(" ");
|
||||
}
|
||||
}
|
||||
@@ -341,23 +354,23 @@ export function getPhonePlaceholder(countryCode: string): string {
|
||||
export function formatInternationalPhone(
|
||||
value: string,
|
||||
dialCode: string,
|
||||
countryCode?: string
|
||||
countryCode?: string,
|
||||
): string {
|
||||
const cleaned = value.replace(/\D/g, "");
|
||||
|
||||
|
||||
// Remove o código de país se já estiver presente
|
||||
let phoneNumber = cleaned;
|
||||
const dialCodeDigits = dialCode.replace(/\D/g, "");
|
||||
|
||||
|
||||
if (phoneNumber.startsWith(dialCodeDigits)) {
|
||||
phoneNumber = phoneNumber.slice(dialCodeDigits.length);
|
||||
}
|
||||
|
||||
|
||||
// Se não temos código de país, usar formatação genérica
|
||||
if (!countryCode) {
|
||||
return formatPhoneByCountry(phoneNumber, "DEFAULT");
|
||||
}
|
||||
|
||||
|
||||
// Aplicar formatação específica do país
|
||||
return formatPhoneByCountry(phoneNumber, countryCode);
|
||||
}
|
||||
@@ -375,15 +388,18 @@ export function unformatPhone(value: string): string {
|
||||
* @param dialCode - Código de discagem (ex: "+55")
|
||||
* @param phoneNumber - Número do telefone (sem código)
|
||||
*/
|
||||
export function combinePhoneWithDialCode(dialCode: string, phoneNumber: string): string {
|
||||
export function combinePhoneWithDialCode(
|
||||
dialCode: string,
|
||||
phoneNumber: string,
|
||||
): string {
|
||||
const cleanedPhone = unformatPhone(phoneNumber);
|
||||
const cleanedDialCode = dialCode.replace(/\D/g, "");
|
||||
|
||||
|
||||
// Remove código de país se já estiver presente
|
||||
if (cleanedPhone.startsWith(cleanedDialCode)) {
|
||||
return `${dialCode}${cleanedPhone.slice(cleanedDialCode.length)}`;
|
||||
}
|
||||
|
||||
|
||||
return `${dialCode}${cleanedPhone}`;
|
||||
}
|
||||
|
||||
@@ -391,7 +407,9 @@ export function combinePhoneWithDialCode(dialCode: string, phoneNumber: string):
|
||||
* Calcula a força da senha
|
||||
* Retorna: 'weak' | 'medium' | 'strong'
|
||||
*/
|
||||
export function checkPasswordStrength(password: string): "weak" | "medium" | "strong" {
|
||||
export function checkPasswordStrength(
|
||||
password: string,
|
||||
): "weak" | "medium" | "strong" {
|
||||
if (password.length < 8) {
|
||||
return "weak";
|
||||
}
|
||||
@@ -473,4 +491,3 @@ export function formatNumber(value: number, locale?: string): string {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
+185
-163
@@ -1,7 +1,7 @@
|
||||
import { z } from "zod";
|
||||
import validator from "validator";
|
||||
import { isValidDocument } from "./validators/document-validator";
|
||||
import sanitizeHtml from "sanitize-html";
|
||||
import validator from "validator";
|
||||
import { z } from "zod";
|
||||
import { isValidDocument } from "./validators/document-validator";
|
||||
|
||||
/**
|
||||
* Schemas de validação usando Zod
|
||||
@@ -59,7 +59,7 @@ export const nameSchema = z
|
||||
.max(100, "Nome deve ter no máximo 100 caracteres")
|
||||
.refine(
|
||||
(name) => /^[A-Za-zÀ-ÿ\s'-]+$/.test(name.trim()),
|
||||
"Nome deve conter apenas letras, espaços e acentos"
|
||||
"Nome deve conter apenas letras, espaços e acentos",
|
||||
)
|
||||
.transform((name) => name.trim());
|
||||
|
||||
@@ -82,7 +82,7 @@ export const donationTierSchema = z.enum(
|
||||
["coffee", "snack", "lunch", "custom"],
|
||||
{
|
||||
message: "Tier de doação inválido",
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Schema para valor customizado
|
||||
@@ -123,7 +123,7 @@ export const createDonationSchema = z
|
||||
{
|
||||
message: "customAmount é obrigatório quando tier é 'custom'",
|
||||
path: ["customAmount"],
|
||||
}
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
@@ -136,7 +136,7 @@ export const createDonationSchema = z
|
||||
{
|
||||
message: "Valor deve estar entre R$ 5,00 e R$ 100.000,00",
|
||||
path: ["customAmount"],
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Schema para verificação de status
|
||||
@@ -160,7 +160,7 @@ export const usernameSchema = z
|
||||
.max(50, "Username deve ter no máximo 50 caracteres")
|
||||
.regex(
|
||||
/^[a-zA-Z0-9_]+$/,
|
||||
"Username deve conter apenas letras, números e underscore"
|
||||
"Username deve conter apenas letras, números e underscore",
|
||||
)
|
||||
.transform((username) => username.toLowerCase().trim());
|
||||
|
||||
@@ -181,7 +181,7 @@ export const passwordSchema = z
|
||||
{
|
||||
message:
|
||||
"Senha deve conter pelo menos 1 letra maiúscula, 1 letra minúscula e 1 número",
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Schema para servidor/personagem no registro
|
||||
@@ -211,7 +211,10 @@ const subscriptionTierFeatureSchema = z.object({
|
||||
.string()
|
||||
.min(2, "featureKey deve ter no mínimo 2 caracteres")
|
||||
.max(100, "featureKey deve ter no máximo 100 caracteres")
|
||||
.regex(featureKeyRegex, "featureKey deve conter apenas letras, números, ponto, hífen ou underscore")
|
||||
.regex(
|
||||
featureKeyRegex,
|
||||
"featureKey deve conter apenas letras, números, ponto, hífen ou underscore",
|
||||
)
|
||||
.transform((key) => key.trim().toLowerCase()),
|
||||
label: z
|
||||
.string()
|
||||
@@ -238,7 +241,10 @@ export const createSubscriptionTierSchema = z
|
||||
.string()
|
||||
.min(2, "Slug deve ter no mínimo 2 caracteres")
|
||||
.max(50, "Slug deve ter no máximo 50 caracteres")
|
||||
.regex(tierSlugRegex, "Slug deve conter apenas letras minúsculas, números e hífen")
|
||||
.regex(
|
||||
tierSlugRegex,
|
||||
"Slug deve conter apenas letras minúsculas, números e hífen",
|
||||
)
|
||||
.transform((value) => value.trim().toLowerCase()),
|
||||
description: z
|
||||
.string()
|
||||
@@ -267,7 +273,7 @@ export const createSubscriptionTierSchema = z
|
||||
{
|
||||
message: "Preço anual deve ser maior ou igual ao mensal",
|
||||
path: ["priceYearly"],
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const updateSubscriptionTierSchema = createSubscriptionTierSchema
|
||||
@@ -277,7 +283,10 @@ export const updateSubscriptionTierSchema = createSubscriptionTierSchema
|
||||
.string()
|
||||
.min(2, "Slug deve ter no mínimo 2 caracteres")
|
||||
.max(50, "Slug deve ter no máximo 50 caracteres")
|
||||
.regex(tierSlugRegex, "Slug deve conter apenas letras minúsculas, números e hífen")
|
||||
.regex(
|
||||
tierSlugRegex,
|
||||
"Slug deve conter apenas letras minúsculas, números e hífen",
|
||||
)
|
||||
.transform((value) => value.trim().toLowerCase())
|
||||
.optional(),
|
||||
features: z
|
||||
@@ -285,7 +294,7 @@ export const updateSubscriptionTierSchema = createSubscriptionTierSchema
|
||||
subscriptionTierFeatureSchema.extend({
|
||||
featureId: z.number().int().positive().optional(),
|
||||
_delete: z.boolean().optional(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.max(50)
|
||||
.optional(),
|
||||
@@ -299,17 +308,17 @@ export const assignSubscriptionTierSchema = z
|
||||
.string()
|
||||
.min(2, "Slug deve ter no mínimo 2 caracteres")
|
||||
.max(50, "Slug deve ter no máximo 50 caracteres")
|
||||
.regex(tierSlugRegex, "Slug deve conter apenas letras minúsculas, números e hífen")
|
||||
.regex(
|
||||
tierSlugRegex,
|
||||
"Slug deve conter apenas letras minúsculas, números e hífen",
|
||||
)
|
||||
.transform((value) => value.trim().toLowerCase())
|
||||
.optional(),
|
||||
})
|
||||
.refine(
|
||||
(data) => data.tierId !== undefined || data.tierSlug !== undefined,
|
||||
{
|
||||
message: "Informe tierId ou tierSlug",
|
||||
path: ["tierId"],
|
||||
}
|
||||
);
|
||||
.refine((data) => data.tierId !== undefined || data.tierSlug !== undefined, {
|
||||
message: "Informe tierId ou tierSlug",
|
||||
path: ["tierId"],
|
||||
});
|
||||
|
||||
// Schema para registro de usuário
|
||||
export const registerSchema = z.object({
|
||||
@@ -361,159 +370,172 @@ export const characterSchema = z.object({
|
||||
// ============================================
|
||||
|
||||
// Schema para criar anúncio (listing)
|
||||
export const createListingSchema = z.object({
|
||||
item_id: z.number().int().positive("ID do item deve ser positivo").optional(), // ID legado (SQLite) - opcional
|
||||
item_id_unified: z.number().int().positive("ID unificado do item deve ser positivo").optional(), // ID unificado da tabela items - preferido
|
||||
item_name: z
|
||||
.string()
|
||||
.min(1, "Nome do item é obrigatório")
|
||||
.max(255, "Nome do item deve ter no máximo 255 caracteres")
|
||||
.transform((name) => name.trim()),
|
||||
quantity: z
|
||||
.number()
|
||||
.int()
|
||||
.positive("Quantidade deve ser maior que zero")
|
||||
.default(1),
|
||||
server_name: z
|
||||
.string()
|
||||
.min(1, "Nome do servidor é obrigatório")
|
||||
.max(50, "Nome do servidor deve ter no máximo 50 caracteres")
|
||||
.transform((name) => name.trim()),
|
||||
price_pix: z
|
||||
.number()
|
||||
.positive("Preço PIX deve ser positivo")
|
||||
.optional()
|
||||
.nullable(),
|
||||
price_tibia_coins: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0, "Preço em Tibia Coins deve ser não negativo")
|
||||
.optional()
|
||||
.nullable(),
|
||||
price_gold_coins: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0, "Preço em Gold Coins deve ser não negativo")
|
||||
.optional()
|
||||
.nullable(),
|
||||
is_auction: z.boolean().optional().default(false),
|
||||
auction_ends_at: z
|
||||
.string()
|
||||
.datetime("Data de término do leilão inválida")
|
||||
.optional()
|
||||
.nullable()
|
||||
.transform((val) => (val ? new Date(val) : null)),
|
||||
description: z
|
||||
.string()
|
||||
.max(5000, "Descrição deve ter no máximo 5000 caracteres")
|
||||
.optional()
|
||||
.nullable()
|
||||
.transform((val) => val?.trim() || null),
|
||||
// Campos para troca por item
|
||||
trade_item_id: z
|
||||
.number()
|
||||
.int()
|
||||
.positive("ID do item de troca deve ser positivo")
|
||||
.optional()
|
||||
.nullable(),
|
||||
trade_item_name: z
|
||||
.string()
|
||||
.min(1, "Nome do item de troca é obrigatório se trade_item_id for fornecido")
|
||||
.max(255, "Nome do item de troca deve ter no máximo 255 caracteres")
|
||||
.optional()
|
||||
.nullable()
|
||||
.transform((name) => name?.trim() || null),
|
||||
trade_item_quantity: z
|
||||
.number()
|
||||
.int()
|
||||
.positive("Quantidade do item de troca deve ser maior que zero")
|
||||
.optional()
|
||||
.nullable()
|
||||
.default(1),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
// Pelo menos um ID (item_id ou item_id_unified) deve ser fornecido
|
||||
if (!data.item_id && !data.item_id_unified) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "Pelo menos um ID do item (item_id ou item_id_unified) deve ser fornecido",
|
||||
path: ["item_id"],
|
||||
}
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
// Se trade_item_id for fornecido, trade_item_name também deve ser
|
||||
if (data.trade_item_id && !data.trade_item_name) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "Nome do item de troca é obrigatório quando ID é fornecido",
|
||||
path: ["trade_item_name"],
|
||||
}
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
// Pelo menos um preço ou troca por item deve ser fornecido
|
||||
return (
|
||||
data.price_pix !== null ||
|
||||
data.price_tibia_coins !== null ||
|
||||
data.price_gold_coins !== null ||
|
||||
data.trade_item_id !== null
|
||||
);
|
||||
},
|
||||
{
|
||||
message: "Pelo menos um método de pagamento (PIX, Tibia Coins ou Gold Coins) deve ser especificado",
|
||||
path: ["price_pix"],
|
||||
}
|
||||
).refine(
|
||||
(data) => {
|
||||
// Se for leilão, deve ter data de término
|
||||
if (data.is_auction && !data.auction_ends_at) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "Data de término do leilão é obrigatória quando is_auction é true",
|
||||
path: ["auction_ends_at"],
|
||||
}
|
||||
);
|
||||
export const createListingSchema = z
|
||||
.object({
|
||||
item_id: z
|
||||
.number()
|
||||
.int()
|
||||
.positive("ID do item deve ser positivo")
|
||||
.optional(), // ID legado (SQLite) - opcional
|
||||
item_id_unified: z
|
||||
.number()
|
||||
.int()
|
||||
.positive("ID unificado do item deve ser positivo")
|
||||
.optional(), // ID unificado da tabela items - preferido
|
||||
item_name: z
|
||||
.string()
|
||||
.min(1, "Nome do item é obrigatório")
|
||||
.max(255, "Nome do item deve ter no máximo 255 caracteres")
|
||||
.transform((name) => name.trim()),
|
||||
quantity: z
|
||||
.number()
|
||||
.int()
|
||||
.positive("Quantidade deve ser maior que zero")
|
||||
.default(1),
|
||||
server_name: z
|
||||
.string()
|
||||
.min(1, "Nome do servidor é obrigatório")
|
||||
.max(50, "Nome do servidor deve ter no máximo 50 caracteres")
|
||||
.transform((name) => name.trim()),
|
||||
price_pix: z
|
||||
.number()
|
||||
.positive("Preço PIX deve ser positivo")
|
||||
.optional()
|
||||
.nullable(),
|
||||
price_tibia_coins: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0, "Preço em Tibia Coins deve ser não negativo")
|
||||
.optional()
|
||||
.nullable(),
|
||||
price_gold_coins: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0, "Preço em Gold Coins deve ser não negativo")
|
||||
.optional()
|
||||
.nullable(),
|
||||
is_auction: z.boolean().optional().default(false),
|
||||
auction_ends_at: z
|
||||
.string()
|
||||
.datetime("Data de término do leilão inválida")
|
||||
.optional()
|
||||
.nullable()
|
||||
.transform((val) => (val ? new Date(val) : null)),
|
||||
description: z
|
||||
.string()
|
||||
.max(5000, "Descrição deve ter no máximo 5000 caracteres")
|
||||
.optional()
|
||||
.nullable()
|
||||
.transform((val) => val?.trim() || null),
|
||||
// Campos para troca por item
|
||||
trade_item_id: z
|
||||
.number()
|
||||
.int()
|
||||
.positive("ID do item de troca deve ser positivo")
|
||||
.optional()
|
||||
.nullable(),
|
||||
trade_item_name: z
|
||||
.string()
|
||||
.min(
|
||||
1,
|
||||
"Nome do item de troca é obrigatório se trade_item_id for fornecido",
|
||||
)
|
||||
.max(255, "Nome do item de troca deve ter no máximo 255 caracteres")
|
||||
.optional()
|
||||
.nullable()
|
||||
.transform((name) => name?.trim() || null),
|
||||
trade_item_quantity: z
|
||||
.number()
|
||||
.int()
|
||||
.positive("Quantidade do item de troca deve ser maior que zero")
|
||||
.optional()
|
||||
.nullable()
|
||||
.default(1),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
// Pelo menos um ID (item_id ou item_id_unified) deve ser fornecido
|
||||
if (!data.item_id && !data.item_id_unified) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Pelo menos um ID do item (item_id ou item_id_unified) deve ser fornecido",
|
||||
path: ["item_id"],
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
// Se trade_item_id for fornecido, trade_item_name também deve ser
|
||||
if (data.trade_item_id && !data.trade_item_name) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "Nome do item de troca é obrigatório quando ID é fornecido",
|
||||
path: ["trade_item_name"],
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
// Pelo menos um preço ou troca por item deve ser fornecido
|
||||
return (
|
||||
data.price_pix !== null ||
|
||||
data.price_tibia_coins !== null ||
|
||||
data.price_gold_coins !== null ||
|
||||
data.trade_item_id !== null
|
||||
);
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Pelo menos um método de pagamento (PIX, Tibia Coins ou Gold Coins) deve ser especificado",
|
||||
path: ["price_pix"],
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
// Se for leilão, deve ter data de término
|
||||
if (data.is_auction && !data.auction_ends_at) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Data de término do leilão é obrigatória quando is_auction é true",
|
||||
path: ["auction_ends_at"],
|
||||
},
|
||||
);
|
||||
|
||||
// Schema para atualizar anúncio
|
||||
export const updateListingSchema = z.object({
|
||||
quantity: z.number().int().positive().optional(),
|
||||
price_pix: z.number().positive().optional().nullable(),
|
||||
price_tibia_coins: z.number().int().min(0).optional().nullable(),
|
||||
price_gold_coins: z.number().int().min(0).optional().nullable(),
|
||||
description: z.string().max(5000).optional().nullable(),
|
||||
listing_status: z
|
||||
.enum(["active", "sold", "cancelled", "in_escrow", "expired"])
|
||||
.optional(),
|
||||
}).refine(
|
||||
(data) => {
|
||||
export const updateListingSchema = z
|
||||
.object({
|
||||
quantity: z.number().int().positive().optional(),
|
||||
price_pix: z.number().positive().optional().nullable(),
|
||||
price_tibia_coins: z.number().int().min(0).optional().nullable(),
|
||||
price_gold_coins: z.number().int().min(0).optional().nullable(),
|
||||
description: z.string().max(5000).optional().nullable(),
|
||||
listing_status: z
|
||||
.enum(["active", "sold", "cancelled", "in_escrow", "expired"])
|
||||
.optional(),
|
||||
})
|
||||
.refine((data) => {
|
||||
// Se atualizando preços, pelo menos um deve ser fornecido ou manter existentes
|
||||
const hasPrice =
|
||||
const _hasPrice =
|
||||
data.price_pix !== undefined ||
|
||||
data.price_tibia_coins !== undefined ||
|
||||
data.price_gold_coins !== undefined;
|
||||
return true; // Permite atualizar sem especificar preços (mantém existentes)
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// Schema para criar lance (bid)
|
||||
export const bidSchema = z.object({
|
||||
listing_id: z.number().int().positive("ID do anúncio deve ser positivo"),
|
||||
bid_amount: z.number().positive("Valor do lance deve ser positivo"),
|
||||
bid_type: z
|
||||
.enum(["pix", "tibia_coins", "gold_coins"])
|
||||
.optional()
|
||||
.nullable(),
|
||||
bid_type: z.enum(["pix", "tibia_coins", "gold_coins"]).optional().nullable(),
|
||||
});
|
||||
|
||||
// ============================================
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* Schemas de validação para Chat usando Zod
|
||||
*/
|
||||
|
||||
import { z } from "zod";
|
||||
import sanitizeHtml from "sanitize-html";
|
||||
import { z } from "zod";
|
||||
|
||||
// Schema para criar chat
|
||||
export const createChatSchema = z.object({
|
||||
@@ -17,7 +17,9 @@ export const sendMessageSchema = z.object({
|
||||
.string()
|
||||
.min(1, "Mensagem não pode estar vazia")
|
||||
.max(1000, "Mensagem muito longa (máximo 1000 caracteres)")
|
||||
.transform((val) => sanitizeHtml(val.trim(), { allowedTags: [], allowedAttributes: {} })),
|
||||
.transform((val) =>
|
||||
sanitizeHtml(val.trim(), { allowedTags: [], allowedAttributes: {} }),
|
||||
),
|
||||
});
|
||||
|
||||
// Schema para query params de mensagens
|
||||
@@ -25,4 +27,3 @@ export const getMessagesSchema = z.object({
|
||||
page: z.coerce.number().int().positive().default(1),
|
||||
limit: z.coerce.number().int().positive().max(100).default(50),
|
||||
});
|
||||
|
||||
@@ -16,20 +16,20 @@ export function isValidCPF(cpf: string): boolean {
|
||||
// Valida primeiro dígito verificador
|
||||
let sum = 0;
|
||||
for (let i = 0; i < 9; i++) {
|
||||
sum += parseInt(cleaned.charAt(i)) * (10 - i);
|
||||
sum += parseInt(cleaned.charAt(i), 10) * (10 - i);
|
||||
}
|
||||
let remainder = (sum * 10) % 11;
|
||||
if (remainder === 10 || remainder === 11) remainder = 0;
|
||||
if (remainder !== parseInt(cleaned.charAt(9))) return false;
|
||||
if (remainder !== parseInt(cleaned.charAt(9), 10)) return false;
|
||||
|
||||
// Valida segundo dígito verificador
|
||||
sum = 0;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
sum += parseInt(cleaned.charAt(i)) * (11 - i);
|
||||
sum += parseInt(cleaned.charAt(i), 10) * (11 - i);
|
||||
}
|
||||
remainder = (sum * 10) % 11;
|
||||
if (remainder === 10 || remainder === 11) remainder = 0;
|
||||
if (remainder !== parseInt(cleaned.charAt(10))) return false;
|
||||
if (remainder !== parseInt(cleaned.charAt(10), 10)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -48,21 +48,21 @@ export function isValidCNPJ(cnpj: string): boolean {
|
||||
const weights1 = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
|
||||
let sum = 0;
|
||||
for (let i = 0; i < 12; i++) {
|
||||
sum += parseInt(cleaned.charAt(i)) * weights1[i];
|
||||
sum += parseInt(cleaned.charAt(i), 10) * weights1[i];
|
||||
}
|
||||
let remainder = sum % 11;
|
||||
const firstDigit = remainder < 2 ? 0 : 11 - remainder;
|
||||
if (firstDigit !== parseInt(cleaned.charAt(12))) return false;
|
||||
if (firstDigit !== parseInt(cleaned.charAt(12), 10)) return false;
|
||||
|
||||
// Valida segundo dígito verificador
|
||||
const weights2 = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
|
||||
sum = 0;
|
||||
for (let i = 0; i < 13; i++) {
|
||||
sum += parseInt(cleaned.charAt(i)) * weights2[i];
|
||||
sum += parseInt(cleaned.charAt(i), 10) * weights2[i];
|
||||
}
|
||||
remainder = sum % 11;
|
||||
const secondDigit = remainder < 2 ? 0 : 11 - remainder;
|
||||
if (secondDigit !== parseInt(cleaned.charAt(13))) return false;
|
||||
if (secondDigit !== parseInt(cleaned.charAt(13), 10)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user