Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion __tests__/lib/finance/creditsManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ describe("lib/finance/creditsManager (com Supabase)", () => {
it("retorna null quando insert payment falha", async () => {
mockGetSupabase.mockReturnValue(
createMockSupabaseClient({
selectUser: { data: { id: "u1" } },
selectUser: { data: { id: "u1", credits_balance: 0 } },
insertPayment: { data: null },
})
);
Expand Down
4 changes: 2 additions & 2 deletions __tests__/lib/stripe.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
const originalEnv = process.env;
const stripeTestEnv = process.env;

jest.mock("stripe", () => {
return jest.fn().mockImplementation(() => ({ _mock: "stripe" }));
});

describe("lib/stripe", () => {
afterEach(() => {
process.env = { ...originalEnv };
process.env = { ...stripeTestEnv };
jest.resetModules();
});

Expand Down
6 changes: 3 additions & 3 deletions __tests__/lib/supabase.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ jest.mock("@supabase/supabase-js", () => ({
createClient: jest.fn(() => ({ mock: true })),
}));

const originalEnv = process.env;
const supabaseTestEnv = process.env;

describe("lib/supabase", () => {
beforeEach(() => {
jest.resetModules();
process.env = { ...originalEnv };
process.env = { ...supabaseTestEnv };
});

afterEach(() => {
process.env = originalEnv;
process.env = supabaseTestEnv;
});

describe("isSupabaseConfigured", () => {
Expand Down
60 changes: 48 additions & 12 deletions app/api/darshan/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { getConnector } from "@/lib/ai";
import { loadMasterPrompt } from "@/lib/darshanPrompt";
import { getConfig } from "@/lib/configStore";
import { PHASE_NAMES } from "@/lib/darshan";
import { getOfflineRevelation } from "@/lib/oracleOffline";
import { composeInstantLight } from "@/lib/sacredRemedy";
import { getSessionFromCookie } from "@/lib/auth";
import {
getCreditsFromCookie,
Expand All @@ -22,6 +22,7 @@ import {
import { logger } from "@/lib/logger";
import { checkAndRecordRateLimit, checkDailyLimit, recordDailyRequest } from "@/lib/usageLimits";
import { isSupabaseConfigured } from "@/lib/supabase";
import { saveRevelation, getRecentInstantLightIds, recordInstantLightUse } from "@/lib/historyStorage";

export const dynamic = "force-dynamic";

Expand Down Expand Up @@ -107,6 +108,9 @@ export async function POST(req: Request) {
birthTime: typeof body.userProfile.birthTime === "string" ? body.userProfile.birthTime : undefined,
}
: {};
const recentSacredIds = Array.isArray(body.recentSacredIds)
? body.recentSacredIds.filter((id: unknown) => typeof id === "string")
: [];

const config = getConfig();
const mockMessages =
Expand All @@ -116,17 +120,42 @@ export async function POST(req: Request) {
: [...MOCK_MESSAGES, ...config.mockMessagesOverride]
: MOCK_MESSAGES;

// IA desativada (mock): 100% offline — NÃO chama getConnector() nem APIs externas.
// IA desativada (mock): Sacred Remedy Engine (único composer).
// Cooldown autônomo: se usuário logado, buscar recentSacredIds/recentStateKeys no servidor e registrar uso.
if (useMock) {
const lastRevelations = history.slice(-5).map((t) => t.darshanMessage);
const phrases = lastRevelations.flatMap((msg) =>
msg.split(/\n\n/).map((s) => s.trim()).filter(Boolean)
);
const recentlyUsedPhrases = phrases.flatMap((p) =>
/o que em você já sabe/i.test(p) ? [p, "O que em você já sabe?"] : [p]
);
const message = getOfflineRevelation(userProfile, userMessage, recentlyUsedPhrases);
return NextResponse.json({ message: message || getMockMessage(mockMessages), phase: 1 } satisfies { message: string; phase: number });
const cookieStore = await cookies();
const session = getSessionFromCookie(cookieStore.toString());
let recentSacredIdsRes = recentSacredIds;
let recentStateKeysRes = Array.isArray(body.recentStateKeys)
? body.recentStateKeys.filter((k: unknown) => typeof k === "string")
: [];
if (session?.email) {
const recent = await getRecentInstantLightIds(session.email, 20);
if (recent.sacredIds.length || recent.stateKeys.length) {
recentSacredIdsRes = recent.sacredIds.length ? recent.sacredIds : recentSacredIdsRes;
recentStateKeysRes = recent.stateKeys.length ? recent.stateKeys : recentStateKeysRes;
}
}
const res = composeInstantLight(userProfile, {
recentSacredIds: recentSacredIdsRes,
recentStateKeys: recentStateKeysRes,
});
if (session?.email && res.sacredId) {
recordInstantLightUse(session.email, { sacredId: res.sacredId, stateKey: res.stateKey ?? undefined }).catch(() => {});
}
const parts: string[] = [];
if (res.sacredText?.trim()) parts.push(res.sacredText.trim());
if (res.insight?.trim()) parts.push(res.insight.trim());
if (res.practice?.trim()) parts.push(res.practice.trim());
if (res.food?.trim()) parts.push(res.food.trim());
if (res.question?.trim()) parts.push(res.question.trim());
const message = parts.length > 0 ? parts.join("\n\n") : getMockMessage(mockMessages);
return NextResponse.json({
message,
phase: 1,
sacredId: res.sacredId,
stateKey: res.stateKey,
} satisfies { message: string; phase: number; sacredId?: string; stateKey?: string });
}

const cookieStore = await cookies();
Expand Down Expand Up @@ -273,8 +302,15 @@ export async function POST(req: Request) {
if (!isSupabaseConfigured()) {
recordDailyRequest(session.email);
}
const finalMessage = message || "Respire. O que em você já sabe?";
if (revelation) {
await saveRevelation(session.email, {
questionText: userMessage || null,
responseText: finalMessage,
});
}
const res = NextResponse.json({
message: message || "Respire. O que em você já sabe?",
message: finalMessage,
phase: revelation ? 1 : (parsed.phase ?? phase),
creditsUsed: creditsPerRevelation,
balance: debitResult.newBalance,
Expand Down
21 changes: 21 additions & 0 deletions app/api/history/count/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* GET /api/history/count — contagem de revelações e leituras do usuário logado.
* Usado para exibir o ícone de histórico quando há dados.
*/

import { NextResponse } from "next/server";
import { cookies } from "next/headers";
import { getSessionFromCookie } from "@/lib/auth";
import { getHistoryCounts } from "@/lib/historyStorage";

export const dynamic = "force-dynamic";

export async function GET() {
const cookieStore = await cookies();
const session = getSessionFromCookie(cookieStore.toString());
if (!session) {
return NextResponse.json({ revelations: 0, readings: 0 });
}
const counts = await getHistoryCounts(session.email);
return NextResponse.json(counts);
}
23 changes: 23 additions & 0 deletions app/api/history/readings/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* GET /api/history/readings — lista leituras (mapa pessoal) do usuário logado.
*/

import { NextResponse } from "next/server";
import { cookies } from "next/headers";
import { getSessionFromCookie } from "@/lib/auth";
import { listReadings } from "@/lib/historyStorage";

export const dynamic = "force-dynamic";

export async function GET(req: Request) {
const cookieStore = await cookies();
const session = getSessionFromCookie(cookieStore.toString());
if (!session) {
return NextResponse.json({ error: "Faça login para ver seu histórico." }, { status: 401 });
}
const { searchParams } = new URL(req.url);
const limit = Math.min(100, Math.max(1, parseInt(searchParams.get("limit") ?? "50", 10) || 50));
const offset = Math.max(0, parseInt(searchParams.get("offset") ?? "0", 10) || 0);
const list = await listReadings(session.email, { limit, offset });
return NextResponse.json({ items: list });
}
23 changes: 23 additions & 0 deletions app/api/history/revelations/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* GET /api/history/revelations — lista revelações (respostas do orb) do usuário logado.
*/

import { NextResponse } from "next/server";
import { cookies } from "next/headers";
import { getSessionFromCookie } from "@/lib/auth";
import { listRevelations } from "@/lib/historyStorage";

export const dynamic = "force-dynamic";

export async function GET(req: Request) {
const cookieStore = await cookies();
const session = getSessionFromCookie(cookieStore.toString());
if (!session) {
return NextResponse.json({ error: "Faça login para ver seu histórico." }, { status: 401 });
}
const { searchParams } = new URL(req.url);
const limit = Math.min(100, Math.max(1, parseInt(searchParams.get("limit") ?? "50", 10) || 50));
const offset = Math.max(0, parseInt(searchParams.get("offset") ?? "0", 10) || 0);
const list = await listRevelations(session.email, { limit, offset });
return NextResponse.json({ items: list });
}
49 changes: 49 additions & 0 deletions app/api/instant-light/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* GET /api/instant-light — Sacred Remedy Engine (motor medicinal offline).
* Não consome créditos; não depende de IA. Motor paralelo ao /api/darshan.
*
* Query: fullName?, birthDate?, birthTime?, birthPlace?, recentSacredIds?, recentStateKeys?
* - Se userProfile existe (nome ou data) → diagnosisPersonal(SymbolicMap) + insight.
* - Se não existe → diagnosisUniversal().
*
* Resposta: sacredText, insight? (se personal), practice, question, sacredId?, stateKey?
*/

import { NextResponse } from "next/server";
import { composeInstantLight } from "@/lib/sacredRemedy";

export const dynamic = "force-dynamic";

export async function GET(req: Request) {
const { searchParams } = new URL(req.url);
const fullName = searchParams.get("fullName") ?? undefined;
const birthDate = searchParams.get("birthDate") ?? undefined;
const birthTime = searchParams.get("birthTime") ?? undefined;
const birthPlace = searchParams.get("birthPlace") ?? undefined;
const recentSacredIdsParam = searchParams.get("recentSacredIds");
const recentStateKeysParam = searchParams.get("recentStateKeys");

const recentSacredIds = recentSacredIdsParam
? recentSacredIdsParam.split(",").map((s) => s.trim()).filter(Boolean)
: [];
const recentStateKeys = recentStateKeysParam
? recentStateKeysParam.split(",").map((s) => s.trim()).filter(Boolean)
: [];

const userProfile =
(fullName?.trim() || birthDate?.trim())
? {
fullName: fullName?.trim() || undefined,
birthDate: birthDate?.trim() || undefined,
birthTime: birthTime?.trim() || undefined,
birthPlace: birthPlace?.trim() || undefined,
}
: null;

const result = composeInstantLight(userProfile, {
recentSacredIds,
recentStateKeys,
});

return NextResponse.json(result);
}
9 changes: 7 additions & 2 deletions app/api/map/personal/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import { getCreditsBalance, debitCredits, logAiUsage, estimateCost, refreshUsdTo
import type { AiUsageProvider } from "@/lib/finance";
import { logger } from "@/lib/logger";
import { getConfig } from "@/lib/configStore";
import { getOfflineReading } from "@/lib/readingOffline";
import { getOfflineReading, getOfflineReadingFullText } from "@/lib/readingOffline";
import { saveReading } from "@/lib/historyStorage";
import { computeVedicChartSimplified } from "@/lib/knowledge/vedic";
import { getRulingNumberFromName, getNumberTraits } from "@/lib/knowledge/numerology";
import { RASHI_NAMES } from "@/lib/knowledge/archetypes";
Expand Down Expand Up @@ -156,11 +157,13 @@ export async function POST(req: Request) {

const useOffline = body.offline === true;
if (useOffline) {
const message = getOfflineReading(profile);
const sections = getOfflineReading(profile);
const message = getOfflineReadingFullText(profile);
const balanceFromCookie = getCreditsFromCookie(cookieHeader);
const balance = await getCreditsBalance(session.email, balanceFromCookie);
return NextResponse.json({
message,
sections,
balance,
creditsUsed: 0,
offline: true,
Expand Down Expand Up @@ -281,6 +284,8 @@ export async function POST(req: Request) {
currentBalanceFromCookie: balance,
});

await saveReading(session.email, message);

const res = NextResponse.json({
message,
balance: debitResult.newBalance,
Expand Down
33 changes: 33 additions & 0 deletions app/api/map/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* POST /api/map — retorna SymbolicMap completo (offline, sem IA, sem créditos).
* Motor simbólico real: Jyotish + Numerologia + Arquétipos.
*/

import { NextResponse } from "next/server";
import { buildSymbolicMap } from "@/lib/symbolic/builder";

export const dynamic = "force-dynamic";

type ProfileInput = {
fullName?: string;
birthDate?: string;
birthTime?: string;
birthPlace?: string;
};

export async function POST(req: Request) {
let body: { profile?: ProfileInput } = {};
try {
body = await req.json();
} catch {
return NextResponse.json({ error: "Corpo inválido." }, { status: 400 });
}
const profile = body.profile ?? {};
const map = buildSymbolicMap({
fullName: profile.fullName,
birthDate: profile.birthDate,
birthTime: profile.birthTime,
birthPlace: profile.birthPlace,
});
return NextResponse.json({ map });
}
30 changes: 30 additions & 0 deletions app/api/reading/career/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* POST /api/reading/career — leitura de carreira offline (mapa + narrativa + action).
*/

import { NextResponse } from "next/server";
import { getCareerReading } from "@/lib/readings/careerReading";
import type { CoreProfile } from "@/lib/core/types";

export const dynamic = "force-dynamic";

export async function POST(req: Request) {
let body: { profile?: CoreProfile } = {};
try {
body = await req.json();
} catch {
return NextResponse.json({ error: "Corpo inválido." }, { status: 400 });
}
const profile: CoreProfile = body.profile ?? {};
const { map, reading, action } = await getCareerReading(profile);
return NextResponse.json({
map: {
core: { providerUsed: map.core.providerUsed, moonRashi: map.core.moonRashi, nakshatra: map.core.nakshatra },
jyotish: map.jyotish,
numerology: map.numerology,
humanDesign: map.humanDesign,
},
reading,
action,
});
}
26 changes: 26 additions & 0 deletions app/api/reading/general/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { NextResponse } from "next/server";
import { getGeneralReading } from "@/lib/readings/generalReading";
import type { CoreProfile } from "@/lib/core/types";

export const dynamic = "force-dynamic";

export async function POST(req: Request) {
let body: { profile?: CoreProfile } = {};
try {
body = await req.json();
} catch {
return NextResponse.json({ error: "Corpo inválido." }, { status: 400 });
}
const profile: CoreProfile = body.profile ?? {};
const { map, reading, action } = await getGeneralReading(profile);
return NextResponse.json({
map: {
core: { providerUsed: map.core.providerUsed, moonRashi: map.core.moonRashi, nakshatra: map.core.nakshatra },
jyotish: map.jyotish,
numerology: map.numerology,
humanDesign: map.humanDesign,
},
reading,
action,
});
}
Loading
Loading