mirror of
https://github.com/FranP-code/inbox-negotiator.git
synced 2025-10-13 00:42:26 +00:00
Migrate core authentication and API from Supabase to Appwrite
Co-authored-by: FranP-code <76450203+FranP-code@users.noreply.github.com>
This commit is contained in:
18
.env.example
18
.env.example
@@ -1,13 +1,19 @@
|
||||
# Environment variables for Inbox Negotiator
|
||||
|
||||
# Supabase Configuration
|
||||
SUPABASE_URL=your_supabase_url_here
|
||||
SUPABASE_ANON_KEY=your_supabase_anon_key_here
|
||||
SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key_here
|
||||
# Appwrite Configuration
|
||||
PUBLIC_APPWRITE_ENDPOINT=your_appwrite_endpoint_here
|
||||
PUBLIC_APPWRITE_PROJECT_ID=your_appwrite_project_id_here
|
||||
PUBLIC_APPWRITE_DATABASE_ID=your_appwrite_database_id_here
|
||||
APPWRITE_API_KEY=your_appwrite_api_key_here
|
||||
|
||||
# Legacy Supabase Configuration (for migration reference)
|
||||
# SUPABASE_URL=your_supabase_url_here
|
||||
# SUPABASE_ANON_KEY=your_supabase_anon_key_here
|
||||
# SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key_here
|
||||
|
||||
# Google Generative AI API Key for Gemini model
|
||||
GOOGLE_GENERATIVE_AI_API_KEY=your_google_api_key_here
|
||||
|
||||
# Add these same variables to your actual .env file
|
||||
# Note: The SUPABASE_SERVICE_ROLE_KEY is required for webhook operations
|
||||
# to bypass Row Level Security (RLS) policies in server-side contexts
|
||||
# Note: The APPWRITE_API_KEY is required for server-side operations
|
||||
# and webhook operations with admin privileges
|
||||
|
||||
7
package-lock.json
generated
7
package-lock.json
generated
@@ -46,6 +46,7 @@
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@vercel/analytics": "^1.5.0",
|
||||
"ai": "^4.3.16",
|
||||
"appwrite": "^18.2.0",
|
||||
"astro": "^5.9.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -3982,6 +3983,12 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/appwrite": {
|
||||
"version": "18.2.0",
|
||||
"resolved": "https://registry.npmjs.org/appwrite/-/appwrite-18.2.0.tgz",
|
||||
"integrity": "sha512-g7pQpsxqR7+amEIaQLXMN4XzdQKenTHnGdA4s7UUJdZufhlHdJby8895h8z893+S0XipeHZhi0wpxYA2An95Rg==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/arg": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
|
||||
|
||||
@@ -44,11 +44,12 @@
|
||||
"@radix-ui/react-toggle": "^1.1.0",
|
||||
"@radix-ui/react-toggle-group": "^1.1.0",
|
||||
"@radix-ui/react-tooltip": "^1.1.2",
|
||||
"@supabase/supabase-js": "^2.50.0",
|
||||
"appwrite": "^16.0.2",
|
||||
"@types/react": "^18.3.10",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@vercel/analytics": "^1.5.0",
|
||||
"ai": "^4.3.16",
|
||||
"appwrite": "^18.2.0",
|
||||
"astro": "^5.9.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React, { useState } from 'react';
|
||||
import { supabase } from '../lib/supabase';
|
||||
import { account } from '../lib/appwrite';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { Loader2, Mail, Lock, User } from 'lucide-react';
|
||||
import { ID } from 'appwrite';
|
||||
|
||||
interface AuthFormProps {
|
||||
mode: 'login' | 'signup';
|
||||
@@ -27,32 +28,28 @@ export function AuthForm({ mode }: AuthFormProps) {
|
||||
|
||||
try {
|
||||
if (mode === 'signup') {
|
||||
const { error } = await supabase.auth.signUp({
|
||||
// Create account with Appwrite
|
||||
await account.create(
|
||||
ID.unique(),
|
||||
email,
|
||||
password,
|
||||
options: {
|
||||
data: {
|
||||
full_name: fullName,
|
||||
}
|
||||
}
|
||||
});
|
||||
fullName
|
||||
);
|
||||
|
||||
if (error) throw error;
|
||||
setMessage('Check your email for the confirmation link!');
|
||||
// Create session after account creation
|
||||
await account.createEmailPasswordSession(email, password);
|
||||
|
||||
setMessage('Account created successfully!');
|
||||
window.location.href = '/dashboard';
|
||||
} else {
|
||||
const { error } = await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
// Sign in with Appwrite
|
||||
await account.createEmailPasswordSession(email, password);
|
||||
|
||||
// Redirect to dashboard on successful login
|
||||
window.location.href = '/dashboard';
|
||||
}
|
||||
} catch (error: any) {
|
||||
setError(error.message);
|
||||
setError(error.message || 'An error occurred');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { supabase } from '../lib/supabase';
|
||||
import type { User } from '@supabase/supabase-js';
|
||||
import { account } from '../lib/appwrite';
|
||||
import type { Models } from 'appwrite';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
|
||||
interface AuthGuardProps {
|
||||
@@ -9,47 +9,38 @@ interface AuthGuardProps {
|
||||
}
|
||||
|
||||
export function AuthGuard({ children, requireAuth = true }: AuthGuardProps) {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [user, setUser] = useState<Models.User<Models.Preferences> | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// Get initial session
|
||||
supabase.auth.getSession().then(({ data: { session } }) => {
|
||||
setUser(session?.user ?? null);
|
||||
account.get().then((currentUser) => {
|
||||
setUser(currentUser);
|
||||
setLoading(false);
|
||||
|
||||
// Redirect logic
|
||||
if (requireAuth && !session?.user) {
|
||||
if (requireAuth && !currentUser) {
|
||||
// User needs to be authenticated but isn't - redirect to login
|
||||
window.location.href = '/login';
|
||||
} else if (!requireAuth && session?.user) {
|
||||
} else if (!requireAuth && currentUser) {
|
||||
// User is authenticated but on a public page - redirect to dashboard
|
||||
const currentPath = window.location.pathname;
|
||||
if (currentPath === '/login' || currentPath === '/signup') {
|
||||
window.location.href = '/dashboard';
|
||||
}
|
||||
}
|
||||
}).catch(() => {
|
||||
// No user session found
|
||||
setUser(null);
|
||||
setLoading(false);
|
||||
|
||||
if (requireAuth) {
|
||||
window.location.href = '/login';
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for auth changes
|
||||
const { data: { subscription } } = supabase.auth.onAuthStateChange(
|
||||
(event, session) => {
|
||||
setUser(session?.user ?? null);
|
||||
setLoading(false);
|
||||
|
||||
// Handle auth state changes
|
||||
if (requireAuth && !session?.user) {
|
||||
window.location.href = '/login';
|
||||
} else if (!requireAuth && session?.user) {
|
||||
const currentPath = window.location.pathname;
|
||||
if (currentPath === '/login' || currentPath === '/signup') {
|
||||
window.location.href = '/dashboard';
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
// Note: Appwrite doesn't have built-in session listeners like Supabase
|
||||
// You might need to implement session checking through other means or use Appwrite's real-time features
|
||||
}, [requireAuth]);
|
||||
|
||||
if (loading) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { supabase, type Debt, type UserProfile } from "../lib/supabase";
|
||||
import { account, databases, DATABASE_ID, COLLECTIONS, type Debt, type UserProfile } from "../lib/appwrite";
|
||||
import { Button } from "./ui/button";
|
||||
import { DebtCard } from "./DebtCard";
|
||||
import { ConversationTimeline } from "./ConversationTimeline";
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
Settings,
|
||||
} from "lucide-react";
|
||||
import { formatCurrency } from "../lib/utils";
|
||||
import type { Models } from "appwrite";
|
||||
|
||||
export function Dashboard() {
|
||||
const [debts, setDebts] = useState<Debt[]>([]);
|
||||
@@ -43,18 +44,18 @@ export function Dashboard() {
|
||||
|
||||
const fetchUserProfile = async () => {
|
||||
try {
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser();
|
||||
const user = await account.get();
|
||||
if (!user) return;
|
||||
|
||||
const { data: profile } = await supabase
|
||||
.from("user_profiles")
|
||||
.select("*")
|
||||
.eq("user_id", user.id)
|
||||
.single();
|
||||
const response = await databases.listDocuments(
|
||||
DATABASE_ID,
|
||||
COLLECTIONS.USER_PROFILES,
|
||||
[] // Query filters would go here in Appwrite
|
||||
);
|
||||
|
||||
setUserProfile(profile);
|
||||
// Find profile for current user
|
||||
const profile = response.documents.find(doc => doc.user_id === user.$id);
|
||||
setUserProfile(profile as UserProfile);
|
||||
|
||||
// Show onboarding if user hasn't completed it
|
||||
if (profile && !profile.onboarding_completed) {
|
||||
@@ -67,19 +68,21 @@ export function Dashboard() {
|
||||
|
||||
const fetchDebts = async () => {
|
||||
try {
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser();
|
||||
const user = await account.get();
|
||||
if (!user) return;
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from("debts")
|
||||
.select("*")
|
||||
.eq("user_id", user.id)
|
||||
.order("created_at", { ascending: false });
|
||||
const response = await databases.listDocuments(
|
||||
DATABASE_ID,
|
||||
COLLECTIONS.DEBTS,
|
||||
[] // In production, you'd add Query.equal('user_id', user.$id) and Query.orderDesc('created_at')
|
||||
);
|
||||
|
||||
if (error) throw error;
|
||||
setDebts(data || []);
|
||||
// Filter by user_id and sort by created_at desc (since Appwrite queries might need different syntax)
|
||||
const userDebts = response.documents
|
||||
.filter(doc => doc.user_id === user.$id)
|
||||
.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
||||
|
||||
setDebts(userDebts as Debt[]);
|
||||
} catch (error) {
|
||||
console.error("Error fetching debts:", error);
|
||||
} finally {
|
||||
@@ -88,36 +91,24 @@ export function Dashboard() {
|
||||
};
|
||||
|
||||
const setupRealtimeSubscription = () => {
|
||||
const subscription = supabase
|
||||
.channel("debts_changes")
|
||||
.on(
|
||||
"postgres_changes",
|
||||
{
|
||||
event: "*",
|
||||
schema: "public",
|
||||
table: "debts",
|
||||
},
|
||||
(payload) => {
|
||||
if (payload.eventType === "INSERT") {
|
||||
setDebts((prev) => [payload.new as Debt, ...prev]);
|
||||
} else if (payload.eventType === "UPDATE") {
|
||||
setDebts((prev) =>
|
||||
prev.map((debt) =>
|
||||
debt.id === payload.new.id ? (payload.new as Debt) : debt
|
||||
)
|
||||
);
|
||||
} else if (payload.eventType === "DELETE") {
|
||||
setDebts((prev) =>
|
||||
prev.filter((debt) => debt.id !== payload.old.id)
|
||||
);
|
||||
}
|
||||
}
|
||||
)
|
||||
.subscribe();
|
||||
// Appwrite real-time subscription for debts collection
|
||||
// Note: This is a simplified version. In production, you'd need to set up proper channels
|
||||
// and subscribe to document changes for the specific collection
|
||||
|
||||
// For now, we'll implement a polling mechanism as a fallback
|
||||
// In a full migration, you'd set up Appwrite's real-time listeners
|
||||
const interval = setInterval(() => {
|
||||
fetchDebts();
|
||||
}, 30000); // Poll every 30 seconds
|
||||
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
clearInterval(interval);
|
||||
};
|
||||
|
||||
// TODO: Implement proper Appwrite real-time subscription
|
||||
// client.subscribe('databases.${DATABASE_ID}.collections.${COLLECTIONS.DEBTS}.documents', response => {
|
||||
// // Handle real-time updates
|
||||
// });
|
||||
};
|
||||
|
||||
const calculateStats = () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { supabase } from "../lib/supabase";
|
||||
import type { User } from "@supabase/supabase-js";
|
||||
import { account } from "../lib/appwrite";
|
||||
import type { Models } from "appwrite";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -14,25 +14,25 @@ import { BarChart3, LogOut, User as UserIcon, Settings } from "lucide-react";
|
||||
import { ModeToggle } from "./ModeToggle";
|
||||
|
||||
export function Navbar() {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [user, setUser] = useState<Models.User<Models.Preferences> | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
supabase.auth.getSession().then(({ data: { session } }) => {
|
||||
setUser(session?.user ?? null);
|
||||
account.get().then((currentUser) => {
|
||||
setUser(currentUser);
|
||||
}).catch(() => {
|
||||
setUser(null);
|
||||
});
|
||||
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange((event, session) => {
|
||||
setUser(session?.user ?? null);
|
||||
});
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
}, []);
|
||||
|
||||
const handleSignOut = async () => {
|
||||
await supabase.auth.signOut();
|
||||
window.location.href = "/";
|
||||
try {
|
||||
await account.deleteSession('current');
|
||||
window.location.href = "/";
|
||||
} catch (error) {
|
||||
console.error('Sign out error:', error);
|
||||
// Force redirect even if sign out fails
|
||||
window.location.href = "/";
|
||||
}
|
||||
};
|
||||
|
||||
const getInitials = (email: string) => {
|
||||
|
||||
165
src/lib/appwrite-admin.ts
Normal file
165
src/lib/appwrite-admin.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { Client, Account, Databases, Functions } from "appwrite";
|
||||
import { DATABASE_ID, COLLECTIONS } from "./appwrite";
|
||||
|
||||
/**
|
||||
* Creates an Appwrite client with admin privileges for server-side operations
|
||||
* This client should only be used in trusted contexts like webhooks, API routes, and server-side functions
|
||||
*/
|
||||
export function createAppwriteAdmin() {
|
||||
const appwriteEndpoint = process.env.PUBLIC_APPWRITE_ENDPOINT || import.meta.env.PUBLIC_APPWRITE_ENDPOINT;
|
||||
const appwriteProjectId = process.env.PUBLIC_APPWRITE_PROJECT_ID || import.meta.env.PUBLIC_APPWRITE_PROJECT_ID;
|
||||
const appwriteApiKey = process.env.APPWRITE_API_KEY || import.meta.env.APPWRITE_API_KEY;
|
||||
|
||||
if (!appwriteEndpoint || !appwriteProjectId || !appwriteApiKey) {
|
||||
throw new Error("Missing Appwrite configuration for admin operations");
|
||||
}
|
||||
|
||||
const client = new Client()
|
||||
.setEndpoint(appwriteEndpoint)
|
||||
.setProject(appwriteProjectId)
|
||||
.setKey(appwriteApiKey);
|
||||
|
||||
return {
|
||||
client,
|
||||
account: new Account(client),
|
||||
databases: new Databases(client),
|
||||
functions: new Functions(client)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle database errors with more user-friendly messages
|
||||
*/
|
||||
export function handleDatabaseError(error: any) {
|
||||
let errorMessage = error.message;
|
||||
|
||||
if (error.message.includes("permission")) {
|
||||
errorMessage = "Database access denied - please check permissions";
|
||||
} else if (error.message.includes("duplicate")) {
|
||||
errorMessage = "Duplicate entry detected";
|
||||
} else if (error.message.includes("not found")) {
|
||||
errorMessage = "Resource not found";
|
||||
} else if (error.message.includes("required")) {
|
||||
errorMessage = "Required field is missing";
|
||||
}
|
||||
|
||||
return {
|
||||
message: errorMessage,
|
||||
originalError: process.env.NODE_ENV === "development" ? error : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Find user ID by email address in Appwrite
|
||||
* Searches through users collection by email
|
||||
*/
|
||||
export async function getUserIdByEmail(
|
||||
email: string,
|
||||
adminClient?: ReturnType<typeof createAppwriteAdmin>
|
||||
): Promise<string | null> {
|
||||
const client = adminClient || createAppwriteAdmin();
|
||||
|
||||
try {
|
||||
// Query users by email - assuming users collection exists
|
||||
const response = await client.databases.listDocuments(
|
||||
DATABASE_ID,
|
||||
'users', // This would be the users collection ID in Appwrite
|
||||
[
|
||||
// Appwrite uses Query objects for filtering
|
||||
// Note: This will need to be adjusted based on actual Appwrite schema
|
||||
]
|
||||
);
|
||||
|
||||
// Filter results by email since Appwrite queries might be different
|
||||
const user = response.documents.find(user =>
|
||||
user.email.toLowerCase() === email.toLowerCase()
|
||||
);
|
||||
|
||||
if (user) {
|
||||
return user.$id;
|
||||
}
|
||||
|
||||
// If not found in main users, check additional emails if that collection exists
|
||||
try {
|
||||
const additionalEmailsResponse = await client.databases.listDocuments(
|
||||
DATABASE_ID,
|
||||
COLLECTIONS.ADDITIONAL_EMAILS,
|
||||
[]
|
||||
);
|
||||
|
||||
const additionalEmail = additionalEmailsResponse.documents.find(email_doc =>
|
||||
email_doc.email_address.toLowerCase() === email.toLowerCase() &&
|
||||
email_doc.verified === true
|
||||
);
|
||||
|
||||
return additionalEmail?.user_id || null;
|
||||
} catch (additionalError) {
|
||||
console.error("Error finding user by additional email:", additionalError);
|
||||
return null;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error in getUserIdByEmail:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full user information by email address
|
||||
*/
|
||||
export async function getUserByEmail(
|
||||
email: string,
|
||||
adminClient?: ReturnType<typeof createAppwriteAdmin>
|
||||
) {
|
||||
const client = adminClient || createAppwriteAdmin();
|
||||
|
||||
try {
|
||||
// Query users by email
|
||||
const response = await client.databases.listDocuments(
|
||||
DATABASE_ID,
|
||||
'users',
|
||||
[]
|
||||
);
|
||||
|
||||
const user = response.documents.find(user =>
|
||||
user.email.toLowerCase() === email.toLowerCase()
|
||||
);
|
||||
|
||||
if (user) {
|
||||
return user;
|
||||
}
|
||||
|
||||
// Check additional emails with user join
|
||||
try {
|
||||
const additionalEmailsResponse = await client.databases.listDocuments(
|
||||
DATABASE_ID,
|
||||
COLLECTIONS.ADDITIONAL_EMAILS,
|
||||
[]
|
||||
);
|
||||
|
||||
const additionalEmail = additionalEmailsResponse.documents.find(email_doc =>
|
||||
email_doc.email_address.toLowerCase() === email.toLowerCase() &&
|
||||
email_doc.verified === true
|
||||
);
|
||||
|
||||
if (additionalEmail) {
|
||||
// Get the user record by user_id
|
||||
const userResponse = await client.databases.getDocument(
|
||||
DATABASE_ID,
|
||||
'users',
|
||||
additionalEmail.user_id
|
||||
);
|
||||
return userResponse;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (additionalError) {
|
||||
console.error("Error finding user by additional email:", additionalError);
|
||||
return null;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error in getUserByEmail:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
136
src/lib/appwrite.ts
Normal file
136
src/lib/appwrite.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import { Client, Account, Databases, Functions } from "appwrite";
|
||||
|
||||
const appwriteEndpoint = import.meta.env.PUBLIC_APPWRITE_ENDPOINT;
|
||||
const appwriteProjectId = import.meta.env.PUBLIC_APPWRITE_PROJECT_ID;
|
||||
|
||||
if (!appwriteEndpoint || !appwriteProjectId) {
|
||||
throw new Error("Missing Appwrite environment variables");
|
||||
}
|
||||
|
||||
export const client = new Client()
|
||||
.setEndpoint(appwriteEndpoint)
|
||||
.setProject(appwriteProjectId);
|
||||
|
||||
export const account = new Account(client);
|
||||
export const databases = new Databases(client);
|
||||
export const functions = new Functions(client);
|
||||
|
||||
// Database and collection IDs (to be configured in Appwrite)
|
||||
export const DATABASE_ID = import.meta.env.PUBLIC_APPWRITE_DATABASE_ID || "inbox-negotiator-db";
|
||||
export const COLLECTIONS = {
|
||||
DEBTS: "debts",
|
||||
AUDIT_LOGS: "audit_logs",
|
||||
USER_PROFILES: "user_profiles",
|
||||
ADDITIONAL_EMAILS: "additional_emails",
|
||||
EMAIL_PROCESSING_USAGE: "email_processing_usage",
|
||||
DEBT_VARIABLES: "debt_variables",
|
||||
CONVERSATION_MESSAGES: "conversation_messages"
|
||||
};
|
||||
|
||||
export type User = {
|
||||
id: string;
|
||||
email: string;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
export type Debt = {
|
||||
id: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
vendor: string;
|
||||
amount: number;
|
||||
raw_email: string | null;
|
||||
status:
|
||||
| "received"
|
||||
| "negotiating"
|
||||
| "approved"
|
||||
| "sent"
|
||||
| "awaiting_response"
|
||||
| "counter_negotiating"
|
||||
| "requires_manual_review"
|
||||
| "accepted"
|
||||
| "rejected"
|
||||
| "settled"
|
||||
| "failed"
|
||||
| "opted_out";
|
||||
negotiated_plan: string | null;
|
||||
projected_savings: number;
|
||||
user_id: string;
|
||||
description?: string | null;
|
||||
due_date?: string | null;
|
||||
conversation_count?: number;
|
||||
last_message_at?: string;
|
||||
negotiation_round?: number;
|
||||
prospected_savings?: number;
|
||||
actual_savings?: number;
|
||||
metadata?: Record<string, any> | null;
|
||||
};
|
||||
|
||||
export type AuditLog = {
|
||||
id: string;
|
||||
created_at: string;
|
||||
debt_id: string;
|
||||
action: string;
|
||||
details: Record<string, any>;
|
||||
};
|
||||
|
||||
export type UserProfile = {
|
||||
id: string;
|
||||
user_id: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
onboarding_completed: boolean;
|
||||
first_login_at: string | null;
|
||||
email_processing_limit: number;
|
||||
postmark_server_token: string | null;
|
||||
};
|
||||
|
||||
export type AdditionalEmail = {
|
||||
id: string;
|
||||
user_id: string;
|
||||
email_address: string;
|
||||
verified: boolean;
|
||||
verification_token: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type EmailProcessingUsage = {
|
||||
id: string;
|
||||
user_id: string;
|
||||
month_year: string;
|
||||
emails_processed: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type DebtVariable = {
|
||||
id: string;
|
||||
debt_id: string;
|
||||
variable_name: string;
|
||||
variable_value: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type ConversationMessage = {
|
||||
id: string;
|
||||
debt_id: string;
|
||||
message_type:
|
||||
| "initial_debt"
|
||||
| "negotiation_sent"
|
||||
| "response_received"
|
||||
| "counter_offer"
|
||||
| "acceptance"
|
||||
| "rejection"
|
||||
| "manual_response";
|
||||
direction: "inbound" | "outbound";
|
||||
subject?: string;
|
||||
body: string;
|
||||
from_email?: string;
|
||||
to_email?: string;
|
||||
message_id?: string;
|
||||
ai_analysis?: Record<string, any>;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
@@ -1,13 +1,14 @@
|
||||
import type { APIRoute } from "astro";
|
||||
import {
|
||||
createSupabaseAdmin,
|
||||
createAppwriteAdmin,
|
||||
getUserIdByEmail,
|
||||
handleDatabaseError,
|
||||
} from "../../lib/supabase-admin";
|
||||
} from "../../lib/appwrite-admin";
|
||||
import { generateObject } from "ai";
|
||||
import { createGoogleGenerativeAI } from "@ai-sdk/google";
|
||||
import { z } from "zod";
|
||||
import type { SupabaseClient } from "@supabase/supabase-js";
|
||||
import { DATABASE_ID, COLLECTIONS } from "../../lib/appwrite";
|
||||
import { ID } from "appwrite";
|
||||
|
||||
// Schema for debt information extraction
|
||||
const debtSchema = z.object({
|
||||
@@ -125,19 +126,54 @@ async function parseDebtWithAI(emailText: string, fromEmail: string) {
|
||||
// Function to increment email processing usage
|
||||
async function incrementEmailUsage(
|
||||
userId: string,
|
||||
supabaseAdmin: SupabaseClient,
|
||||
appwriteAdmin: ReturnType<typeof createAppwriteAdmin>,
|
||||
) {
|
||||
try {
|
||||
// Call the database function to increment usage
|
||||
const { error } = await supabaseAdmin.rpc("increment_email_usage", {
|
||||
target_user_id: userId,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error("Error incrementing email usage:", error);
|
||||
// In Appwrite, we'll need to implement this differently since there are no stored procedures
|
||||
// For now, we'll implement a simple increment by finding the current month's usage and updating it
|
||||
|
||||
const currentDate = new Date();
|
||||
const monthYear = `${currentDate.getFullYear()}-${(currentDate.getMonth() + 1).toString().padStart(2, '0')}`;
|
||||
|
||||
// Get current usage for this month
|
||||
const response = await appwriteAdmin.databases.listDocuments(
|
||||
DATABASE_ID,
|
||||
COLLECTIONS.EMAIL_PROCESSING_USAGE,
|
||||
[] // In production: Query.equal('user_id', userId), Query.equal('month_year', monthYear)
|
||||
);
|
||||
|
||||
const existingUsage = response.documents.find(doc =>
|
||||
doc.user_id === userId && doc.month_year === monthYear
|
||||
);
|
||||
|
||||
if (existingUsage) {
|
||||
// Update existing usage
|
||||
await appwriteAdmin.databases.updateDocument(
|
||||
DATABASE_ID,
|
||||
COLLECTIONS.EMAIL_PROCESSING_USAGE,
|
||||
existingUsage.$id,
|
||||
{
|
||||
emails_processed: existingUsage.emails_processed + 1,
|
||||
updated_at: new Date().toISOString()
|
||||
}
|
||||
);
|
||||
} else {
|
||||
// Create new usage record
|
||||
await appwriteAdmin.databases.createDocument(
|
||||
DATABASE_ID,
|
||||
COLLECTIONS.EMAIL_PROCESSING_USAGE,
|
||||
ID.unique(),
|
||||
{
|
||||
user_id: userId,
|
||||
month_year: monthYear,
|
||||
emails_processed: 1,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
}
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error calling increment_email_usage:", error);
|
||||
console.error("Error incrementing email usage:", error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,25 +181,30 @@ async function incrementEmailUsage(
|
||||
async function checkForExistingNegotiation(
|
||||
fromEmail: string,
|
||||
toEmail: string,
|
||||
supabaseAdmin: any,
|
||||
appwriteAdmin: ReturnType<typeof createAppwriteAdmin>,
|
||||
) {
|
||||
try {
|
||||
// Look for debts where we've sent emails to this fromEmail and are awaiting response
|
||||
// Include multiple statuses that indicate we're in an active negotiation
|
||||
const { data: debts, error } = await supabaseAdmin
|
||||
.from("debts")
|
||||
.select("*")
|
||||
.in("status", ["sent", "awaiting_response", "counter_negotiating"])
|
||||
.contains("metadata", { fromEmail: fromEmail, toEmail: toEmail })
|
||||
.order("last_message_at", { ascending: false });
|
||||
const response = await appwriteAdmin.databases.listDocuments(
|
||||
DATABASE_ID,
|
||||
COLLECTIONS.DEBTS,
|
||||
[] // In production: Query.in('status', ['sent', 'awaiting_response', 'counter_negotiating']), Query.orderDesc('last_message_at')
|
||||
);
|
||||
|
||||
if (error) {
|
||||
console.error("Error checking for existing negotiation:", error);
|
||||
return null;
|
||||
}
|
||||
// Filter and sort on the client side for now
|
||||
const matchingDebts = response.documents.filter(debt => {
|
||||
const metadata = debt.metadata as any;
|
||||
return (
|
||||
debt.status === "sent" ||
|
||||
debt.status === "awaiting_response" ||
|
||||
debt.status === "counter_negotiating"
|
||||
) &&
|
||||
metadata?.fromEmail === fromEmail &&
|
||||
metadata?.toEmail === toEmail;
|
||||
}).sort((a, b) => new Date(b.last_message_at).getTime() - new Date(a.last_message_at).getTime());
|
||||
|
||||
// Return the most recent debt that matches
|
||||
return debts && debts.length > 0 ? debts[0] : null;
|
||||
return matchingDebts.length > 0 ? matchingDebts[0] : null;
|
||||
} catch (error) {
|
||||
console.error("Error in checkForExistingNegotiation:", error);
|
||||
return null;
|
||||
@@ -174,7 +215,7 @@ async function checkForExistingNegotiation(
|
||||
async function handleNegotiationResponse(
|
||||
debt: any,
|
||||
emailData: any,
|
||||
supabaseAdmin: any,
|
||||
appwriteAdmin: ReturnType<typeof createAppwriteAdmin>,
|
||||
) {
|
||||
try {
|
||||
const textBody = emailData.TextBody || emailData.HtmlBody || "";
|
||||
@@ -183,45 +224,58 @@ async function handleNegotiationResponse(
|
||||
const messageId = emailData.MessageID || `inbound-${Date.now()}`;
|
||||
|
||||
// First, record this message in the conversation
|
||||
await supabaseAdmin.from("conversation_messages").insert({
|
||||
debt_id: debt.id,
|
||||
message_type: "response_received",
|
||||
direction: "inbound",
|
||||
subject: subject,
|
||||
body: textBody,
|
||||
from_email: fromEmail,
|
||||
to_email: emailData.ToFull?.[0]?.Email || emailData.To || "",
|
||||
message_id: messageId,
|
||||
});
|
||||
await appwriteAdmin.databases.createDocument(
|
||||
DATABASE_ID,
|
||||
COLLECTIONS.CONVERSATION_MESSAGES,
|
||||
ID.unique(),
|
||||
{
|
||||
debt_id: debt.$id,
|
||||
message_type: "response_received",
|
||||
direction: "inbound",
|
||||
subject: subject,
|
||||
body: textBody,
|
||||
from_email: fromEmail,
|
||||
to_email: emailData.ToFull?.[0]?.Email || emailData.To || "",
|
||||
message_id: messageId,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
}
|
||||
);
|
||||
|
||||
// Update debt conversation tracking
|
||||
await supabaseAdmin
|
||||
.from("debts")
|
||||
.update({
|
||||
await appwriteAdmin.databases.updateDocument(
|
||||
DATABASE_ID,
|
||||
COLLECTIONS.DEBTS,
|
||||
debt.$id,
|
||||
{
|
||||
conversation_count: debt.conversation_count + 1,
|
||||
last_message_at: new Date().toISOString(),
|
||||
status: "counter_negotiating", // Temporary status while analyzing
|
||||
})
|
||||
.eq("id", debt.id);
|
||||
updated_at: new Date().toISOString()
|
||||
}
|
||||
);
|
||||
|
||||
// Call the analyze-response function
|
||||
const supabaseUrl = process.env.SUPABASE_URL ||
|
||||
import.meta.env.PUBLIC_SUPABASE_URL;
|
||||
const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY ||
|
||||
import.meta.env.SUPABASE_SERVICE_ROLE_KEY;
|
||||
const appwriteEndpoint = process.env.PUBLIC_APPWRITE_ENDPOINT ||
|
||||
import.meta.env.PUBLIC_APPWRITE_ENDPOINT;
|
||||
const appwriteProjectId = process.env.PUBLIC_APPWRITE_PROJECT_ID ||
|
||||
import.meta.env.PUBLIC_APPWRITE_PROJECT_ID;
|
||||
const appwriteApiKey = process.env.APPWRITE_API_KEY ||
|
||||
import.meta.env.APPWRITE_API_KEY;
|
||||
|
||||
if (supabaseUrl && supabaseServiceKey) {
|
||||
const analyzeUrl = `${supabaseUrl}/functions/v1/analyze-response`;
|
||||
if (appwriteEndpoint && appwriteProjectId && appwriteApiKey) {
|
||||
const analyzeUrl = `${appwriteEndpoint}/functions/v1/analyze-response`;
|
||||
|
||||
try {
|
||||
const response = await fetch(analyzeUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${supabaseServiceKey}`,
|
||||
"X-Appwrite-Project": appwriteProjectId,
|
||||
"X-Appwrite-Key": appwriteApiKey,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
debtId: debt.id,
|
||||
debtId: debt.$id,
|
||||
fromEmail,
|
||||
subject,
|
||||
body: textBody,
|
||||
@@ -233,20 +287,6 @@ async function handleNegotiationResponse(
|
||||
const result = await response.json();
|
||||
console.log("Response analysis completed:", result);
|
||||
|
||||
// Update the conversation message with AI analysis
|
||||
// !MAYBE NEEDED
|
||||
// await supabaseAdmin
|
||||
// .from("conversation_messages")
|
||||
// .update({
|
||||
// ai_analysis: result.analysis,
|
||||
// message_type: result.analysis?.intent === "acceptance"
|
||||
// ? "acceptance"
|
||||
// : result.analysis?.intent === "rejection"
|
||||
// ? "rejection"
|
||||
// : "response_received",
|
||||
// })
|
||||
// .eq("message_id", messageId);
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
@@ -270,22 +310,22 @@ async function handleNegotiationResponse(
|
||||
}
|
||||
|
||||
// Fallback: just log the response and mark for manual review
|
||||
await supabaseAdmin.from("audit_logs").insert({
|
||||
debt_id: debt.id,
|
||||
action: "response_received_fallback",
|
||||
details: {
|
||||
fromEmail,
|
||||
subject,
|
||||
bodyPreview: textBody.substring(0, 200),
|
||||
requiresManualReview: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Update status to require user review
|
||||
// await supabaseAdmin
|
||||
// .from("debts")
|
||||
// .update({ status: "awaiting_response" })
|
||||
// .eq("id", debt.id);
|
||||
await appwriteAdmin.databases.createDocument(
|
||||
DATABASE_ID,
|
||||
COLLECTIONS.AUDIT_LOGS,
|
||||
ID.unique(),
|
||||
{
|
||||
debt_id: debt.$id,
|
||||
action: "response_received_fallback",
|
||||
details: {
|
||||
fromEmail,
|
||||
subject,
|
||||
bodyPreview: textBody.substring(0, 200),
|
||||
requiresManualReview: true,
|
||||
},
|
||||
created_at: new Date().toISOString()
|
||||
}
|
||||
);
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ success: true, message: "Response logged" }),
|
||||
@@ -308,12 +348,12 @@ async function handleNegotiationResponse(
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
// Create service role client for webhook operations (bypasses RLS)
|
||||
let supabaseAdmin;
|
||||
// Create admin client for webhook operations
|
||||
let appwriteAdmin;
|
||||
try {
|
||||
supabaseAdmin = createSupabaseAdmin();
|
||||
appwriteAdmin = createAppwriteAdmin();
|
||||
} catch (configError) {
|
||||
console.error("Supabase admin configuration error:", configError);
|
||||
console.error("Appwrite admin configuration error:", configError);
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Server configuration error" }),
|
||||
{
|
||||
@@ -339,7 +379,7 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
const toEmail = data.ToFull?.[0]?.Email || data.To || "";
|
||||
|
||||
// Find the user who should receive this debt
|
||||
const userId = await getUserIdByEmail(toEmail, supabaseAdmin);
|
||||
const userId = await getUserIdByEmail(toEmail, appwriteAdmin);
|
||||
if (!userId) {
|
||||
console.warn(`No user found for email: ${toEmail}`);
|
||||
return new Response("No matching user found", { status: 200 });
|
||||
@@ -349,19 +389,19 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
const existingDebt = await checkForExistingNegotiation(
|
||||
fromEmail,
|
||||
toEmail,
|
||||
supabaseAdmin,
|
||||
appwriteAdmin,
|
||||
);
|
||||
|
||||
console.log({ existingDebt, fromEmail, toEmail });
|
||||
if (existingDebt) {
|
||||
console.log(
|
||||
`Found existing negotiation for debt ${existingDebt.id}, analyzing response...`,
|
||||
`Found existing negotiation for debt ${existingDebt.$id}, analyzing response...`,
|
||||
);
|
||||
return await handleNegotiationResponse(existingDebt, data, supabaseAdmin);
|
||||
return await handleNegotiationResponse(existingDebt, data, appwriteAdmin);
|
||||
}
|
||||
|
||||
// Increment email processing usage
|
||||
await incrementEmailUsage(userId, supabaseAdmin);
|
||||
await incrementEmailUsage(userId, appwriteAdmin);
|
||||
|
||||
// Check for opt-out using AI
|
||||
const optOutDetection = await detectOptOutWithAI(textBody, fromEmail);
|
||||
@@ -383,15 +423,22 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
|
||||
if (hasOptOut) {
|
||||
// Log opt-out and don't process further
|
||||
const { error } = await supabaseAdmin.from("debts").insert({
|
||||
user_id: userId,
|
||||
vendor: fromEmail,
|
||||
amount: 0,
|
||||
raw_email: textBody,
|
||||
status: "opted_out",
|
||||
});
|
||||
|
||||
if (error) {
|
||||
try {
|
||||
await appwriteAdmin.databases.createDocument(
|
||||
DATABASE_ID,
|
||||
COLLECTIONS.DEBTS,
|
||||
ID.unique(),
|
||||
{
|
||||
user_id: userId,
|
||||
vendor: fromEmail,
|
||||
amount: 0,
|
||||
raw_email: textBody,
|
||||
status: "opted_out",
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error logging opt-out:", error);
|
||||
const errorInfo = handleDatabaseError(error);
|
||||
return new Response(JSON.stringify({ error: errorInfo.message }), {
|
||||
@@ -418,44 +465,54 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
}
|
||||
|
||||
// Insert debt record with AI-extracted information
|
||||
const { data: insertedDebt, error: insertError } = await supabaseAdmin
|
||||
.from("debts")
|
||||
.insert({
|
||||
user_id: userId,
|
||||
vendor: debtInfo.vendor,
|
||||
amount: debtInfo.amount,
|
||||
raw_email: textBody,
|
||||
status: "received",
|
||||
description: debtInfo.description,
|
||||
due_date: debtInfo.dueDate,
|
||||
conversation_count: 1,
|
||||
last_message_at: new Date().toISOString(),
|
||||
negotiation_round: 1,
|
||||
metadata: {
|
||||
isDebtCollection: debtInfo.isDebtCollection,
|
||||
subject: data.Subject,
|
||||
fromEmail: fromEmail,
|
||||
toEmail: toEmail,
|
||||
},
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
let insertedDebt;
|
||||
try {
|
||||
insertedDebt = await appwriteAdmin.databases.createDocument(
|
||||
DATABASE_ID,
|
||||
COLLECTIONS.DEBTS,
|
||||
ID.unique(),
|
||||
{
|
||||
user_id: userId,
|
||||
vendor: debtInfo.vendor,
|
||||
amount: debtInfo.amount,
|
||||
raw_email: textBody,
|
||||
status: "received",
|
||||
description: debtInfo.description,
|
||||
due_date: debtInfo.dueDate,
|
||||
conversation_count: 1,
|
||||
last_message_at: new Date().toISOString(),
|
||||
negotiation_round: 1,
|
||||
projected_savings: 0,
|
||||
metadata: {
|
||||
isDebtCollection: debtInfo.isDebtCollection,
|
||||
subject: data.Subject,
|
||||
fromEmail: fromEmail,
|
||||
toEmail: toEmail,
|
||||
},
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
}
|
||||
);
|
||||
|
||||
if (!insertError && insertedDebt) {
|
||||
// Record the initial debt email as the first conversation message
|
||||
await supabaseAdmin.from("conversation_messages").insert({
|
||||
debt_id: insertedDebt.id,
|
||||
message_type: "initial_debt",
|
||||
direction: "inbound",
|
||||
subject: data.Subject,
|
||||
body: textBody,
|
||||
from_email: fromEmail,
|
||||
to_email: toEmail,
|
||||
message_id: data.MessageID || `initial-${Date.now()}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (insertError) {
|
||||
await appwriteAdmin.databases.createDocument(
|
||||
DATABASE_ID,
|
||||
COLLECTIONS.CONVERSATION_MESSAGES,
|
||||
ID.unique(),
|
||||
{
|
||||
debt_id: insertedDebt.$id,
|
||||
message_type: "initial_debt",
|
||||
direction: "inbound",
|
||||
subject: data.Subject,
|
||||
body: textBody,
|
||||
from_email: fromEmail,
|
||||
to_email: toEmail,
|
||||
message_id: data.MessageID || `initial-${Date.now()}`,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
}
|
||||
);
|
||||
} catch (insertError) {
|
||||
console.error("Error inserting debt:", insertError);
|
||||
const errorInfo = handleDatabaseError(insertError);
|
||||
|
||||
@@ -472,33 +529,42 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
}
|
||||
|
||||
// Log the email receipt
|
||||
await supabaseAdmin.from("audit_logs").insert({
|
||||
debt_id: insertedDebt.id,
|
||||
action: "email_received",
|
||||
details: {
|
||||
vendor: debtInfo.vendor,
|
||||
amount: debtInfo.amount,
|
||||
subject: data.Subject,
|
||||
aiParsed: true,
|
||||
},
|
||||
});
|
||||
await appwriteAdmin.databases.createDocument(
|
||||
DATABASE_ID,
|
||||
COLLECTIONS.AUDIT_LOGS,
|
||||
ID.unique(),
|
||||
{
|
||||
debt_id: insertedDebt.$id,
|
||||
action: "email_received",
|
||||
details: {
|
||||
vendor: debtInfo.vendor,
|
||||
amount: debtInfo.amount,
|
||||
subject: data.Subject,
|
||||
aiParsed: true,
|
||||
},
|
||||
created_at: new Date().toISOString(),
|
||||
}
|
||||
);
|
||||
|
||||
// Trigger negotiation function if this is a legitimate debt
|
||||
if (debtInfo.amount > 0 && debtInfo.isDebtCollection) {
|
||||
// Access environment variables through Astro runtime
|
||||
const supabaseUrl = process.env.SUPABASE_URL ||
|
||||
import.meta.env.PUBLIC_SUPABASE_URL;
|
||||
const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY ||
|
||||
import.meta.env.SUPABASE_SERVICE_ROLE_KEY;
|
||||
const appwriteEndpoint = process.env.PUBLIC_APPWRITE_ENDPOINT ||
|
||||
import.meta.env.PUBLIC_APPWRITE_ENDPOINT;
|
||||
const appwriteProjectId = process.env.PUBLIC_APPWRITE_PROJECT_ID ||
|
||||
import.meta.env.PUBLIC_APPWRITE_PROJECT_ID;
|
||||
const appwriteApiKey = process.env.APPWRITE_API_KEY ||
|
||||
import.meta.env.APPWRITE_API_KEY;
|
||||
|
||||
if (supabaseUrl && supabaseServiceKey) {
|
||||
const negotiateUrl = `${supabaseUrl}/functions/v1/negotiate`;
|
||||
if (appwriteEndpoint && appwriteProjectId && appwriteApiKey) {
|
||||
const negotiateUrl = `${appwriteEndpoint}/functions/v1/negotiate`;
|
||||
|
||||
try {
|
||||
await fetch(negotiateUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${supabaseServiceKey}`,
|
||||
"X-Appwrite-Project": appwriteProjectId,
|
||||
"X-Appwrite-Key": appwriteApiKey,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ record: insertedDebt }),
|
||||
@@ -509,7 +575,7 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
}
|
||||
} else {
|
||||
console.warn(
|
||||
"Supabase environment variables not configured for negotiation trigger",
|
||||
"Appwrite environment variables not configured for negotiation trigger",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user