Initial commit
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getAgentSettings, updateAgentSettings } from '@/lib/firestore';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// TODO: Get userId from auth token
|
||||
const userId = 'test-user-id'; // Replace with actual auth
|
||||
const settings = await getAgentSettings(userId);
|
||||
return NextResponse.json({ settings });
|
||||
} catch (error) {
|
||||
console.error('Error fetching agent settings:', error);
|
||||
return NextResponse.json({ error: 'Failed to fetch settings' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
// TODO: Get userId from auth token
|
||||
const userId = 'test-user-id'; // Replace with actual auth
|
||||
|
||||
await updateAgentSettings(userId, body);
|
||||
const settings = await getAgentSettings(userId);
|
||||
return NextResponse.json({ settings });
|
||||
} catch (error) {
|
||||
console.error('Error updating agent settings:', error);
|
||||
return NextResponse.json({ error: 'Failed to update settings' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getBookedAppointments } from '@/lib/firestore';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// TODO: Get userId from auth token
|
||||
const userId = 'test-user-id'; // Replace with actual auth
|
||||
const appointments = await getBookedAppointments(userId);
|
||||
return NextResponse.json({ appointments });
|
||||
} catch (error) {
|
||||
console.error('Error fetching appointments:', error);
|
||||
return NextResponse.json({ error: 'Failed to fetch appointments' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getAuth, signInWithEmailAndPassword } from 'firebase/auth';
|
||||
import { app } from '@/lib/firebase';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { email, password } = await request.json();
|
||||
|
||||
if (!email || !password) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Missing email or password' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const auth = getAuth(app);
|
||||
|
||||
const userCredential = await signInWithEmailAndPassword(auth, email, password);
|
||||
const user = userCredential.user;
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
user: {
|
||||
uid: user.uid,
|
||||
email: user.email,
|
||||
name: user.displayName,
|
||||
},
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
} catch (error: any) {
|
||||
console.error('Login error:', error);
|
||||
|
||||
let message = 'Login failed';
|
||||
|
||||
if (error?.code === 'auth/user-not-found') {
|
||||
message = 'No user found with that email';
|
||||
} else if (error?.code === 'auth/wrong-password') {
|
||||
message = 'Incorrect password';
|
||||
} else if (error?.code === 'auth/invalid-credential') {
|
||||
message = 'Invalid credentials';
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: message }, { status: 400 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export async function GET(_request: NextRequest) {
|
||||
// MVP placeholder: always "not authenticated"
|
||||
// Later we will read a token (cookie/header) and return the real user.
|
||||
return NextResponse.json(
|
||||
{
|
||||
user: null,
|
||||
isAuthenticated: false,
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getAuth, createUserWithEmailAndPassword, updateProfile } from 'firebase/auth';
|
||||
import { app } from '@/lib/firebase';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { email, password, name } = await request.json();
|
||||
|
||||
if (!email || !password || !name) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Missing email, password or name' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const auth = getAuth(app);
|
||||
|
||||
const userCredential = await createUserWithEmailAndPassword(auth, email, password);
|
||||
|
||||
if (auth.currentUser) {
|
||||
await updateProfile(auth.currentUser, { displayName: name });
|
||||
}
|
||||
|
||||
const user = userCredential.user;
|
||||
|
||||
// Firebase client SDK already keeps the session via cookies/local storage on client.
|
||||
// For MVP, we just return basic user info.
|
||||
return NextResponse.json(
|
||||
{
|
||||
user: {
|
||||
uid: user.uid,
|
||||
email: user.email,
|
||||
name: user.displayName,
|
||||
},
|
||||
},
|
||||
{ status: 201 }
|
||||
);
|
||||
} catch (error: any) {
|
||||
console.error('Register error:', error);
|
||||
|
||||
const message =
|
||||
error?.code === 'auth/email-already-in-use'
|
||||
? 'Email already in use'
|
||||
: 'Registration failed';
|
||||
|
||||
return NextResponse.json({ error: message }, { status: 400 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { GoogleGenerativeAI } from '@google/generative-ai';
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const { messages } = await req.json();
|
||||
|
||||
if (!process.env.GEMINI_API_KEY) {
|
||||
return NextResponse.json({ error: 'Gemini API key not configured' }, { status: 500 });
|
||||
}
|
||||
|
||||
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);
|
||||
const model = genAI.getGenerativeModel({ model: 'gemini-2.0-flash' });
|
||||
|
||||
const history = messages.slice(0, -1).map((msg: any) => ({
|
||||
role: msg.role === 'assistant' ? 'model' : 'user',
|
||||
parts: [{ text: msg.content }],
|
||||
}));
|
||||
|
||||
const userMessage = messages[messages.length - 1].content;
|
||||
|
||||
const chat = model.startChat({
|
||||
history: history,
|
||||
generationConfig: {
|
||||
temperature: 0.7,
|
||||
maxOutputTokens: 1000,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await chat.sendMessage(userMessage);
|
||||
const text = result.response.text();
|
||||
|
||||
return NextResponse.json({ text });
|
||||
} catch (error: any) {
|
||||
console.error('Error in chat API:', error);
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getDefaultLimits, updateDefaultLimits } from '@/lib/firestore';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// TODO: Get userId from auth token
|
||||
const userId = 'test-user-id'; // Replace with actual auth
|
||||
const limits = await getDefaultLimits(userId);
|
||||
return NextResponse.json({ limits });
|
||||
} catch (error) {
|
||||
console.error('Error fetching credit limits:', error);
|
||||
return NextResponse.json({ error: 'Failed to fetch limits' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
// TODO: Get userId from auth token
|
||||
const userId = 'test-user-id'; // Replace with actual auth
|
||||
|
||||
await updateDefaultLimits(userId, body);
|
||||
const limits = await getDefaultLimits(userId);
|
||||
return NextResponse.json({ limits });
|
||||
} catch (error) {
|
||||
console.error('Error updating credit limits:', error);
|
||||
return NextResponse.json({ error: 'Failed to update limits' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import {
|
||||
createCreditTransaction,
|
||||
getUser,
|
||||
updateUserCredits,
|
||||
} from '@/lib/firestore';
|
||||
import { Timestamp } from 'firebase/firestore';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// TODO: Get userId from auth token
|
||||
const userId = 'test-user-id'; // Replace with actual auth
|
||||
const user = await getUser(userId);
|
||||
return NextResponse.json({ balance: user?.creditBalance ?? 0 });
|
||||
} catch (error) {
|
||||
console.error('Error fetching credit balance:', error);
|
||||
return NextResponse.json({ error: 'Failed to fetch balance' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
// TODO: Get userId from auth token
|
||||
const userId = 'test-user-id'; // Replace with actual auth
|
||||
const amount = Number(body?.amount);
|
||||
|
||||
if (!Number.isFinite(amount) || amount <= 0) {
|
||||
return NextResponse.json({ error: 'Invalid amount' }, { status: 400 });
|
||||
}
|
||||
|
||||
const user = await getUser(userId);
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const newBalance = user.creditBalance + amount;
|
||||
await updateUserCredits(userId, newBalance);
|
||||
|
||||
await createCreditTransaction({
|
||||
userId,
|
||||
amount,
|
||||
type: 'purchase',
|
||||
balanceAfter: newBalance,
|
||||
createdAt: Timestamp.now(),
|
||||
});
|
||||
|
||||
return NextResponse.json({ balance: newBalance });
|
||||
} catch (error) {
|
||||
console.error('Error adding credits:', error);
|
||||
return NextResponse.json({ error: 'Failed to add credits' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { collection, getDocs, getFirestore, query, where, Timestamp } from 'firebase/firestore';
|
||||
import { app } from '@/lib/firebase';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
// TODO: Secure this route using a secret header for Vercel Cron.
|
||||
const db = getFirestore(app);
|
||||
const now = Timestamp.now();
|
||||
|
||||
// TODO: Add index for scheduledAt + status.
|
||||
const tasksQuery = query(
|
||||
collection(db, 'callTasks'),
|
||||
where('status', '==', 'pending'),
|
||||
where('scheduledAt', '<=', now)
|
||||
);
|
||||
|
||||
const snapshot = await getDocs(tasksQuery);
|
||||
const origin = request.headers.get('origin') ?? 'http://localhost:3000';
|
||||
|
||||
for (const docSnap of snapshot.docs) {
|
||||
const task = docSnap.data() as any;
|
||||
|
||||
// Trigger the call (TODO: include auth)
|
||||
await fetch(`${origin}/api/vapi/create-call`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
phoneNumber: task.phoneNumber,
|
||||
userId: task.userId,
|
||||
scheduledTaskId: docSnap.id,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({ processed: snapshot.size });
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import {
|
||||
getNotificationSettings,
|
||||
updateNotificationSettings,
|
||||
} from '@/lib/firestore';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// TODO: Get userId from auth token
|
||||
const userId = 'test-user-id'; // Replace with actual auth
|
||||
const settings = await getNotificationSettings(userId);
|
||||
return NextResponse.json({ settings });
|
||||
} catch (error) {
|
||||
console.error('Error fetching notification settings:', error);
|
||||
return NextResponse.json({ error: 'Failed to fetch settings' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
// TODO: Get userId from auth token
|
||||
const userId = 'test-user-id'; // Replace with actual auth
|
||||
|
||||
await updateNotificationSettings(userId, body);
|
||||
const settings = await getNotificationSettings(userId);
|
||||
return NextResponse.json({ settings });
|
||||
} catch (error) {
|
||||
console.error('Error updating notification settings:', error);
|
||||
return NextResponse.json({ error: 'Failed to update settings' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import {
|
||||
deleteCallTask,
|
||||
getCallTask,
|
||||
updateCallTask,
|
||||
} from '@/lib/firestore';
|
||||
import { Timestamp } from 'firebase/firestore';
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
// TODO: Get userId from auth token
|
||||
const userId = 'test-user-id'; // Replace with actual auth
|
||||
const task = await getCallTask(params.id);
|
||||
|
||||
if (!task || task.userId !== userId) {
|
||||
return NextResponse.json({ error: 'Task not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ task });
|
||||
} catch (error) {
|
||||
console.error('Error fetching call task:', error);
|
||||
return NextResponse.json({ error: 'Failed to fetch task' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
// TODO: Get userId from auth token
|
||||
const userId = 'test-user-id'; // Replace with actual auth
|
||||
|
||||
const existing = await getCallTask(params.id);
|
||||
if (!existing || existing.userId !== userId) {
|
||||
return NextResponse.json({ error: 'Task not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const updates: Record<string, unknown> = { ...body };
|
||||
if (updates.scheduledAt) {
|
||||
updates.scheduledAt = Timestamp.fromDate(new Date(updates.scheduledAt as string));
|
||||
}
|
||||
|
||||
await updateCallTask(params.id, updates);
|
||||
const task = await getCallTask(params.id);
|
||||
return NextResponse.json({ task });
|
||||
} catch (error) {
|
||||
console.error('Error updating call task:', error);
|
||||
return NextResponse.json({ error: 'Failed to update task' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
// TODO: Get userId from auth token
|
||||
const userId = 'test-user-id'; // Replace with actual auth
|
||||
const existing = await getCallTask(params.id);
|
||||
if (!existing || existing.userId !== userId) {
|
||||
return NextResponse.json({ error: 'Task not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
await deleteCallTask(params.id);
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Error deleting call task:', error);
|
||||
return NextResponse.json({ error: 'Failed to delete task' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getCallTasks, createCallTask, getUser } from '@/lib/firestore';
|
||||
import { Timestamp } from 'firebase/firestore';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// TODO: Get userId from auth token
|
||||
const userId = 'test-user-id'; // Replace with actual auth
|
||||
const tasks = await getCallTasks(userId);
|
||||
return NextResponse.json({ tasks });
|
||||
} catch (error) {
|
||||
console.error('Error fetching call tasks:', error);
|
||||
return NextResponse.json({ error: 'Failed to fetch tasks' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const userId = 'test-user-id'; // TODO: Get from auth token
|
||||
|
||||
const {
|
||||
businessName,
|
||||
phoneNumber,
|
||||
preferredLanguage,
|
||||
callGoals,
|
||||
scheduledAt,
|
||||
timezone,
|
||||
maxMinutes,
|
||||
allowRecalls,
|
||||
maxRecalls,
|
||||
} = body;
|
||||
|
||||
if (!businessName || !phoneNumber || !callGoals || !scheduledAt || !maxMinutes) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Missing required fields' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Validate user has enough credits
|
||||
const user = await getUser(userId);
|
||||
if (!user || user.creditBalance < maxMinutes) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Insufficient credits' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const taskId = await createCallTask({
|
||||
userId,
|
||||
businessName,
|
||||
phoneNumber,
|
||||
preferredLanguage: preferredLanguage || 'English',
|
||||
callGoals,
|
||||
scheduledAt: Timestamp.fromDate(new Date(scheduledAt)),
|
||||
timezone: timezone || undefined,
|
||||
maxMinutes,
|
||||
maxCredits: maxMinutes, // 1:1 for v1
|
||||
allowRecalls: allowRecalls || false,
|
||||
maxRecalls: maxRecalls || 0,
|
||||
recallsUsed: 0,
|
||||
status: 'pending',
|
||||
createdAt: Timestamp.now(),
|
||||
updatedAt: Timestamp.now(),
|
||||
});
|
||||
|
||||
return NextResponse.json({ taskId });
|
||||
} catch (error) {
|
||||
console.error('Error creating call task:', error);
|
||||
return NextResponse.json({ error: 'Failed to create task' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import Stripe from 'stripe';
|
||||
|
||||
const stripeSecretKey = process.env.STRIPE_SECRET_KEY;
|
||||
|
||||
const stripe = stripeSecretKey
|
||||
? new Stripe(stripeSecretKey, { apiVersion: '2024-06-20' })
|
||||
: null;
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
if (!stripe) {
|
||||
return NextResponse.json({ error: 'Stripe is not configured.' }, { status: 500 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const credits = Number(body?.credits);
|
||||
const amount = Number(body?.amount);
|
||||
const userId = body?.userId as string | undefined;
|
||||
|
||||
// TODO: Get userId from auth token
|
||||
const resolvedUserId = userId ?? 'test-user-id';
|
||||
|
||||
if (!resolvedUserId || !Number.isFinite(credits) || credits <= 0) {
|
||||
return NextResponse.json({ error: 'Invalid checkout payload.' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!Number.isFinite(amount) || amount <= 0) {
|
||||
return NextResponse.json({ error: 'Invalid amount.' }, { status: 400 });
|
||||
}
|
||||
|
||||
const origin = request.headers.get('origin') ?? 'http://localhost:3000';
|
||||
|
||||
const session = await stripe.checkout.sessions.create({
|
||||
mode: 'payment',
|
||||
payment_method_types: ['card'],
|
||||
line_items: [
|
||||
{
|
||||
quantity: 1,
|
||||
price_data: {
|
||||
currency: 'eur',
|
||||
unit_amount: amount * 100,
|
||||
product_data: {
|
||||
name: `${credits} HolaCompi Credits`,
|
||||
description: 'Pay-as-you-go voice call credits',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
success_url: `${origin}/dashboard/credits?success=true`,
|
||||
cancel_url: `${origin}/dashboard/credits?canceled=true`,
|
||||
metadata: {
|
||||
userId: resolvedUserId,
|
||||
credits: credits.toString(),
|
||||
amount: amount.toString(),
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({ sessionId: session.id, url: session.url });
|
||||
} catch (error) {
|
||||
console.error('Stripe checkout error:', error);
|
||||
return NextResponse.json({ error: 'Failed to create checkout session.' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import Stripe from 'stripe';
|
||||
import { NextResponse } from 'next/server';
|
||||
import { Timestamp } from 'firebase/firestore';
|
||||
import {
|
||||
createCreditTransaction,
|
||||
createPurchase,
|
||||
getUser,
|
||||
updateUserCredits,
|
||||
} from '@/lib/firestore';
|
||||
|
||||
const stripeSecretKey = process.env.STRIPE_SECRET_KEY;
|
||||
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
|
||||
|
||||
const stripe = stripeSecretKey
|
||||
? new Stripe(stripeSecretKey, { apiVersion: '2024-06-20' })
|
||||
: null;
|
||||
|
||||
export async function POST(request: Request) {
|
||||
if (!stripe || !webhookSecret) {
|
||||
return NextResponse.json({ error: 'Stripe webhook not configured.' }, { status: 500 });
|
||||
}
|
||||
|
||||
const signature = request.headers.get('stripe-signature');
|
||||
if (!signature) {
|
||||
return NextResponse.json({ error: 'Missing Stripe signature.' }, { status: 400 });
|
||||
}
|
||||
|
||||
const payload = await request.text();
|
||||
|
||||
let event: Stripe.Event;
|
||||
try {
|
||||
event = stripe.webhooks.constructEvent(payload, signature, webhookSecret);
|
||||
} catch (error) {
|
||||
console.error('Stripe webhook signature error:', error);
|
||||
return NextResponse.json({ error: 'Invalid signature.' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (event.type === 'checkout.session.completed') {
|
||||
const session = event.data.object as Stripe.Checkout.Session;
|
||||
const metadata = session.metadata ?? {};
|
||||
const userId = metadata.userId;
|
||||
const credits = Number(metadata.credits);
|
||||
const amount = Number(metadata.amount);
|
||||
|
||||
if (!userId || !Number.isFinite(credits) || credits <= 0) {
|
||||
return NextResponse.json({ error: 'Invalid checkout metadata.' }, { status: 400 });
|
||||
}
|
||||
|
||||
const user = await getUser(userId);
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found.' }, { status: 404 });
|
||||
}
|
||||
|
||||
const newBalance = user.creditBalance + credits;
|
||||
await updateUserCredits(userId, newBalance);
|
||||
|
||||
await createCreditTransaction({
|
||||
userId,
|
||||
amount: credits,
|
||||
type: 'purchase',
|
||||
balanceAfter: newBalance,
|
||||
createdAt: Timestamp.now(),
|
||||
});
|
||||
|
||||
await createPurchase({
|
||||
userId,
|
||||
credits,
|
||||
amount: Number.isFinite(amount) ? amount : credits,
|
||||
currency: session.currency ?? 'eur',
|
||||
stripeSessionId: session.id,
|
||||
createdAt: Timestamp.now(),
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({ received: true });
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { createCallRecord, getAgentSettings, getUser } from '@/lib/firestore';
|
||||
|
||||
const VAPI_BASE_URL = 'https://api.vapi.ai';
|
||||
const vapiPrivateKey = process.env.VAPI_PRIVATE_KEY;
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
if (!vapiPrivateKey) {
|
||||
return NextResponse.json({ error: 'Vapi not configured.' }, { status: 500 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const phoneNumber = body?.phoneNumber as string | undefined;
|
||||
const scheduledTaskId = body?.scheduledTaskId as string | undefined;
|
||||
|
||||
if (!phoneNumber) {
|
||||
return NextResponse.json({ error: 'Phone number is required.' }, { status: 400 });
|
||||
}
|
||||
|
||||
// TODO: Get userId from auth token
|
||||
const userId = body?.userId ?? 'test-user-id';
|
||||
const user = await getUser(userId);
|
||||
if (!user || user.creditBalance < 1) {
|
||||
return NextResponse.json({ error: 'Insufficient credits.' }, { status: 400 });
|
||||
}
|
||||
|
||||
const agentSettings = await getAgentSettings(userId);
|
||||
|
||||
const assistantPayload = {
|
||||
name: agentSettings?.agentName ?? 'HolaCompi',
|
||||
instructions:
|
||||
agentSettings?.agentInstructions ??
|
||||
'You are HolaCompi, a helpful Spanish friend who makes phone calls on behalf of users.',
|
||||
model: 'gpt-4o-mini',
|
||||
voice: {
|
||||
provider: 'openai',
|
||||
voice: 'alloy',
|
||||
},
|
||||
language: agentSettings?.primaryCallLanguage ?? 'English',
|
||||
};
|
||||
|
||||
// Create assistant dynamically based on agent settings
|
||||
const assistantResponse = await fetch(`${VAPI_BASE_URL}/assistant`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${vapiPrivateKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(assistantPayload),
|
||||
});
|
||||
|
||||
if (!assistantResponse.ok) {
|
||||
const errorText = await assistantResponse.text();
|
||||
return NextResponse.json({ error: errorText }, { status: 500 });
|
||||
}
|
||||
|
||||
const assistant = await assistantResponse.json();
|
||||
|
||||
// Initiate outbound call via Vapi API
|
||||
const callResponse = await fetch(`${VAPI_BASE_URL}/call`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${vapiPrivateKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
assistantId: assistant.id,
|
||||
phoneNumber,
|
||||
// TODO: Adjust payload based on Vapi call schema (customer/from numbers)
|
||||
}),
|
||||
});
|
||||
|
||||
if (!callResponse.ok) {
|
||||
const errorText = await callResponse.text();
|
||||
return NextResponse.json({ error: errorText }, { status: 500 });
|
||||
}
|
||||
|
||||
const callData = await callResponse.json();
|
||||
|
||||
await createCallRecord({
|
||||
userId,
|
||||
phoneNumber,
|
||||
status: 'initiated',
|
||||
vapiCallId: callData.id,
|
||||
assistantId: assistant.id,
|
||||
scheduledTaskId,
|
||||
});
|
||||
|
||||
return NextResponse.json({ status: callData.status ?? 'initiated', callId: callData.id });
|
||||
} catch (error) {
|
||||
console.error('Vapi create call error:', error);
|
||||
return NextResponse.json({ error: 'Failed to create call.' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { Timestamp } from 'firebase/firestore';
|
||||
import { getCallByVapiCallId, getUser, updateCallRecord, updateUserCredits } from '@/lib/firestore';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const payload = await request.json();
|
||||
const eventType = payload?.event || payload?.type;
|
||||
const callPayload = payload?.call || payload?.data?.call || payload;
|
||||
const vapiCallId = callPayload?.id as string | undefined;
|
||||
|
||||
if (!vapiCallId) {
|
||||
return NextResponse.json({ error: 'Missing call id.' }, { status: 400 });
|
||||
}
|
||||
|
||||
const callRecord = await getCallByVapiCallId(vapiCallId);
|
||||
if (!callRecord) {
|
||||
return NextResponse.json({ error: 'Call record not found.' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (eventType === 'call-started' || eventType === 'call.started') {
|
||||
await updateCallRecord(callRecord.id, {
|
||||
status: 'started',
|
||||
startedAt: Timestamp.now(),
|
||||
});
|
||||
}
|
||||
|
||||
if (eventType === 'call-failed' || eventType === 'call.failed') {
|
||||
await updateCallRecord(callRecord.id, {
|
||||
status: 'failed',
|
||||
endedAt: Timestamp.now(),
|
||||
});
|
||||
}
|
||||
|
||||
if (eventType === 'call-ended' || eventType === 'call.ended') {
|
||||
const durationSeconds =
|
||||
Number(callPayload?.durationSeconds) ||
|
||||
Number(callPayload?.duration) ||
|
||||
0;
|
||||
const creditsUsed = Math.ceil(durationSeconds / 60);
|
||||
|
||||
await updateCallRecord(callRecord.id, {
|
||||
status: 'ended',
|
||||
endedAt: Timestamp.now(),
|
||||
durationSeconds,
|
||||
creditsUsed,
|
||||
recordingUrl: callPayload?.recordingUrl || callPayload?.recording?.url,
|
||||
transcript: callPayload?.transcript || callPayload?.summary,
|
||||
});
|
||||
|
||||
const user = await getUser(callRecord.userId);
|
||||
if (user) {
|
||||
const newBalance = Math.max(0, user.creditBalance - creditsUsed);
|
||||
await updateUserCredits(callRecord.userId, newBalance);
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ received: true });
|
||||
} catch (error) {
|
||||
console.error('Vapi webhook error:', error);
|
||||
return NextResponse.json({ error: 'Webhook processing failed.' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user