mirror of
https://github.com/FranP-code/inbox-negotiator.git
synced 2025-10-13 00:42:26 +00:00
Complete core Appwrite migration with documentation and fixes
Co-authored-by: FranP-code <76450203+FranP-code@users.noreply.github.com>
This commit is contained in:
119
APPWRITE_MIGRATION.md
Normal file
119
APPWRITE_MIGRATION.md
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
# Appwrite Migration Progress
|
||||||
|
|
||||||
|
## Completed Migrations
|
||||||
|
|
||||||
|
### 1. Dependencies
|
||||||
|
- ✅ Installed Appwrite SDK v16.0.2
|
||||||
|
- ✅ Removed @supabase/supabase-js dependency
|
||||||
|
- ✅ Updated package.json
|
||||||
|
|
||||||
|
### 2. Core Configuration
|
||||||
|
- ✅ Created `src/lib/appwrite.ts` - Main client configuration
|
||||||
|
- ✅ Created `src/lib/appwrite-admin.ts` - Server-side admin operations
|
||||||
|
- ✅ Updated environment variables in `.env.example`
|
||||||
|
|
||||||
|
### 3. Authentication Components
|
||||||
|
- ✅ `src/components/AuthForm.tsx` - Migrated to Appwrite Account API
|
||||||
|
- ✅ `src/components/AuthGuard.tsx` - Updated session management
|
||||||
|
- ✅ `src/components/Navbar.tsx` - Updated user state handling
|
||||||
|
|
||||||
|
### 4. Database Operations
|
||||||
|
- ✅ `src/components/Dashboard.tsx` - Partially migrated database queries
|
||||||
|
- ✅ `src/pages/api/postmark.ts` - Migrated webhook API to use Appwrite
|
||||||
|
- ✅ `src/components/DebtCard.tsx` - Migrated debt operations and function calls
|
||||||
|
- 🔄 `src/components/Configuration.tsx` - Partially migrated user data fetching
|
||||||
|
|
||||||
|
### 5. Function Calls
|
||||||
|
- ✅ Updated function invocation from Supabase to Appwrite Functions API
|
||||||
|
- ✅ Changed authentication headers from Bearer tokens to X-Appwrite-Project/X-Appwrite-Key
|
||||||
|
|
||||||
|
## Required Appwrite Setup
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
```bash
|
||||||
|
PUBLIC_APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1
|
||||||
|
PUBLIC_APPWRITE_PROJECT_ID=your_project_id
|
||||||
|
PUBLIC_APPWRITE_DATABASE_ID=your_database_id
|
||||||
|
APPWRITE_API_KEY=your_api_key
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Collections to Create
|
||||||
|
1. **debts** - Main debt records
|
||||||
|
2. **audit_logs** - Action logging
|
||||||
|
3. **user_profiles** - User settings and preferences
|
||||||
|
4. **additional_emails** - Secondary email addresses
|
||||||
|
5. **email_processing_usage** - Monthly usage tracking
|
||||||
|
6. **debt_variables** - Custom debt variables
|
||||||
|
7. **conversation_messages** - Email conversation history
|
||||||
|
8. **users** - User personal data
|
||||||
|
|
||||||
|
### Functions to Migrate
|
||||||
|
1. **negotiate** - AI debt negotiation logic
|
||||||
|
2. **approve-debt** - Debt approval workflow
|
||||||
|
3. **send-email** - Email sending functionality
|
||||||
|
4. **analyze-response** - Email response analysis
|
||||||
|
5. **test-extraction** - Debt information extraction testing
|
||||||
|
|
||||||
|
## Remaining Tasks
|
||||||
|
|
||||||
|
### Components Not Yet Migrated
|
||||||
|
- `src/components/ConversationTimeline.tsx`
|
||||||
|
- `src/components/OnboardingDialog.tsx`
|
||||||
|
- `src/components/ManualResponseDialog.tsx`
|
||||||
|
- `src/components/DebtTimeline.tsx`
|
||||||
|
- `src/components/RealtimeTestButton.tsx`
|
||||||
|
- `src/components/ExtractionTester.tsx`
|
||||||
|
|
||||||
|
### Migration Notes
|
||||||
|
|
||||||
|
#### Authentication
|
||||||
|
- Replaced `supabase.auth.signUp()` with `account.create()` + `account.createEmailPasswordSession()`
|
||||||
|
- Replaced `supabase.auth.signInWithPassword()` with `account.createEmailPasswordSession()`
|
||||||
|
- Replaced `supabase.auth.getUser()` with `account.get()`
|
||||||
|
- Replaced `supabase.auth.signOut()` with `account.deleteSession('current')`
|
||||||
|
|
||||||
|
#### Database Operations
|
||||||
|
- Replaced `supabase.from().select()` with `databases.listDocuments()`
|
||||||
|
- Replaced `supabase.from().insert()` with `databases.createDocument()`
|
||||||
|
- Replaced `supabase.from().update()` with `databases.updateDocument()`
|
||||||
|
- Note: Appwrite queries use different syntax than Supabase filters
|
||||||
|
|
||||||
|
#### Function Calls
|
||||||
|
- Replaced `supabase.functions.invoke()` with `functions.createExecution()`
|
||||||
|
- Changed authentication from Authorization header to X-Appwrite-Project/X-Appwrite-Key headers
|
||||||
|
|
||||||
|
#### Real-time Updates
|
||||||
|
- Supabase real-time subscriptions need to be replaced with Appwrite real-time
|
||||||
|
- Currently using polling as a temporary fallback
|
||||||
|
|
||||||
|
## Testing Required
|
||||||
|
|
||||||
|
1. **Authentication Flow**
|
||||||
|
- User registration
|
||||||
|
- User login/logout
|
||||||
|
- Session persistence
|
||||||
|
|
||||||
|
2. **Database Operations**
|
||||||
|
- Debt creation and updates
|
||||||
|
- User profile management
|
||||||
|
- Audit logging
|
||||||
|
|
||||||
|
3. **Function Execution**
|
||||||
|
- Email sending
|
||||||
|
- AI negotiation
|
||||||
|
- Response analysis
|
||||||
|
|
||||||
|
4. **API Endpoints**
|
||||||
|
- Postmark webhook processing
|
||||||
|
- Email parsing and storage
|
||||||
|
|
||||||
|
## Production Deployment Checklist
|
||||||
|
|
||||||
|
1. Set up Appwrite project and database
|
||||||
|
2. Create all required collections with proper schemas
|
||||||
|
3. Deploy Appwrite Functions (migrated from Supabase Edge Functions)
|
||||||
|
4. Configure environment variables
|
||||||
|
5. Set up proper permissions and security rules
|
||||||
|
6. Test all functionality end-to-end
|
||||||
|
7. Migrate data from Supabase to Appwrite
|
||||||
|
8. Update DNS/domain configuration if needed
|
||||||
41
README.md
41
README.md
@@ -7,17 +7,18 @@ An AI-powered system that automatically negotiates debt collections and billing
|
|||||||
- **AI Email Processing**: Automatically parses incoming emails to extract debt information using Google's Gemini AI
|
- **AI Email Processing**: Automatically parses incoming emails to extract debt information using Google's Gemini AI
|
||||||
- **Automated Negotiation**: Triggers negotiation workflows for legitimate debt collection notices
|
- **Automated Negotiation**: Triggers negotiation workflows for legitimate debt collection notices
|
||||||
- **Webhook Integration**: Seamlessly processes emails through Postmark webhook integration
|
- **Webhook Integration**: Seamlessly processes emails through Postmark webhook integration
|
||||||
- **Row Level Security**: Secure database operations with proper authentication handling
|
- **Secure Database Operations**: Uses Appwrite's document-level permissions for secure data access
|
||||||
|
|
||||||
## Environment Setup
|
## Environment Setup
|
||||||
|
|
||||||
Copy `.env.example` to `.env` and configure the following variables:
|
Copy `.env.example` to `.env` and configure the following variables:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Supabase Configuration
|
# Appwrite Configuration
|
||||||
SUPABASE_URL=your_supabase_url_here
|
PUBLIC_APPWRITE_ENDPOINT=your_appwrite_endpoint_here
|
||||||
SUPABASE_ANON_KEY=your_supabase_anon_key_here
|
PUBLIC_APPWRITE_PROJECT_ID=your_appwrite_project_id_here
|
||||||
SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key_here
|
PUBLIC_APPWRITE_DATABASE_ID=your_appwrite_database_id_here
|
||||||
|
APPWRITE_API_KEY=your_appwrite_api_key_here
|
||||||
|
|
||||||
# Google Generative AI API Key for Gemini model
|
# Google Generative AI API Key for Gemini model
|
||||||
GOOGLE_GENERATIVE_AI_API_KEY=your_google_api_key_here
|
GOOGLE_GENERATIVE_AI_API_KEY=your_google_api_key_here
|
||||||
@@ -25,11 +26,23 @@ GOOGLE_GENERATIVE_AI_API_KEY=your_google_api_key_here
|
|||||||
|
|
||||||
### Required Environment Variables
|
### Required Environment Variables
|
||||||
|
|
||||||
- `SUPABASE_URL`: Your Supabase project URL
|
- `PUBLIC_APPWRITE_ENDPOINT`: Your Appwrite instance endpoint (e.g., https://cloud.appwrite.io/v1)
|
||||||
- `SUPABASE_ANON_KEY`: Supabase anonymous key for client-side operations
|
- `PUBLIC_APPWRITE_PROJECT_ID`: Appwrite project ID
|
||||||
- `SUPABASE_SERVICE_ROLE_KEY`: Supabase service role key for server-side operations (bypasses RLS)
|
- `PUBLIC_APPWRITE_DATABASE_ID`: Appwrite database ID for the application
|
||||||
|
- `APPWRITE_API_KEY`: Appwrite API key for server-side operations (webhooks, functions)
|
||||||
- `GOOGLE_GENERATIVE_AI_API_KEY`: Google API key for AI processing
|
- `GOOGLE_GENERATIVE_AI_API_KEY`: Google API key for AI processing
|
||||||
|
|
||||||
|
## Migration from Supabase
|
||||||
|
|
||||||
|
This application has been migrated from Supabase to Appwrite. Key changes include:
|
||||||
|
|
||||||
|
- **Authentication**: Migrated from Supabase Auth to Appwrite Account API
|
||||||
|
- **Database**: Moved from Supabase tables to Appwrite collections
|
||||||
|
- **Functions**: Migrated from Supabase Edge Functions to Appwrite Functions
|
||||||
|
- **Real-time**: Updated from Supabase channels to Appwrite real-time subscriptions
|
||||||
|
|
||||||
|
For detailed migration notes, see [APPWRITE_MIGRATION.md](./APPWRITE_MIGRATION.md).
|
||||||
|
|
||||||
## Webhook Configuration
|
## Webhook Configuration
|
||||||
|
|
||||||
The `/api/postmark` endpoint handles incoming email webhooks from Postmark. It:
|
The `/api/postmark` endpoint handles incoming email webhooks from Postmark. It:
|
||||||
@@ -37,23 +50,23 @@ The `/api/postmark` endpoint handles incoming email webhooks from Postmark. It:
|
|||||||
1. Validates incoming email data
|
1. Validates incoming email data
|
||||||
2. Processes opt-out requests
|
2. Processes opt-out requests
|
||||||
3. Uses AI to extract debt information
|
3. Uses AI to extract debt information
|
||||||
4. Stores processed data in Supabase
|
4. Stores processed data in Appwrite
|
||||||
5. Triggers automated negotiation workflows
|
5. Triggers automated negotiation workflows
|
||||||
|
|
||||||
### RLS (Row Level Security) Handling
|
### Security Handling
|
||||||
|
|
||||||
The webhook uses a service role client to bypass RLS policies, ensuring server-side operations can write to the database without user authentication. This is essential for webhook operations where no user session exists.
|
The webhook uses an Appwrite admin client with API key authentication, ensuring server-side operations can write to the database without user authentication. This is essential for webhook operations where no user session exists.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
pnpm install
|
npm install
|
||||||
|
|
||||||
# Start development server
|
# Start development server
|
||||||
pnpm dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
Ensure all environment variables are configured in your deployment environment, especially the `SUPABASE_SERVICE_ROLE_KEY` which is critical for webhook operations.
|
Ensure all environment variables are configured in your deployment environment, especially the `APPWRITE_API_KEY` which is critical for webhook operations.
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
supabase,
|
account,
|
||||||
|
databases,
|
||||||
|
DATABASE_ID,
|
||||||
|
COLLECTIONS,
|
||||||
type AdditionalEmail,
|
type AdditionalEmail,
|
||||||
type UserProfile,
|
type UserProfile,
|
||||||
type EmailProcessingUsage,
|
type EmailProcessingUsage,
|
||||||
} from "../lib/supabase";
|
} from "../lib/appwrite";
|
||||||
|
import { ID } from "appwrite";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
import { Input } from "./ui/input";
|
import { Input } from "./ui/input";
|
||||||
import { Label } from "./ui/label";
|
import { Label } from "./ui/label";
|
||||||
@@ -65,44 +69,48 @@ export function Configuration() {
|
|||||||
|
|
||||||
const fetchUserData = async () => {
|
const fetchUserData = async () => {
|
||||||
try {
|
try {
|
||||||
const {
|
const user = await account.get();
|
||||||
data: { user },
|
|
||||||
} = await supabase.auth.getUser();
|
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
|
|
||||||
// Fetch user profile
|
// Fetch user profile
|
||||||
const { data: profileData } = await supabase
|
const profileResponse = await databases.listDocuments(
|
||||||
.from("user_profiles")
|
DATABASE_ID,
|
||||||
.select("*")
|
COLLECTIONS.USER_PROFILES,
|
||||||
.eq("user_id", user.id)
|
[] // In production: Query.equal('user_id', user.$id)
|
||||||
.single();
|
);
|
||||||
|
const profileData = profileResponse.documents.find(doc => doc.user_id === user.$id);
|
||||||
|
|
||||||
// Fetch user personal data
|
// Fetch user personal data from users collection
|
||||||
const { data: userData } = await supabase
|
const usersResponse = await databases.listDocuments(
|
||||||
.from("users")
|
DATABASE_ID,
|
||||||
.select("*")
|
'users', // Assuming users collection exists
|
||||||
.eq("id", user.id)
|
[] // In production: Query.equal('id', user.$id)
|
||||||
.single();
|
);
|
||||||
|
const userData = usersResponse.documents.find(doc => doc.id === user.$id);
|
||||||
|
|
||||||
// Fetch additional emails
|
// Fetch additional emails
|
||||||
const { data: emailsData } = await supabase
|
const emailsResponse = await databases.listDocuments(
|
||||||
.from("additional_emails")
|
DATABASE_ID,
|
||||||
.select("*")
|
COLLECTIONS.ADDITIONAL_EMAILS,
|
||||||
.eq("user_id", user.id)
|
[] // In production: Query.equal('user_id', user.$id), Query.orderDesc('created_at')
|
||||||
.order("created_at", { ascending: false });
|
);
|
||||||
|
const emailsData = emailsResponse.documents.filter(doc => doc.user_id === user.$id)
|
||||||
|
.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
||||||
|
|
||||||
// Fetch current month usage
|
// Fetch current month usage
|
||||||
const currentMonth = new Date().toISOString().slice(0, 7); // YYYY-MM
|
const currentMonth = new Date().toISOString().slice(0, 7); // YYYY-MM
|
||||||
const { data: usageData } = await supabase
|
const usageResponse = await databases.listDocuments(
|
||||||
.from("email_processing_usage")
|
DATABASE_ID,
|
||||||
.select("*")
|
COLLECTIONS.EMAIL_PROCESSING_USAGE,
|
||||||
.eq("user_id", user.id)
|
[] // In production: Query.equal('user_id', user.$id), Query.equal('month_year', currentMonth)
|
||||||
.eq("month_year", currentMonth)
|
);
|
||||||
.single();
|
const usageData = usageResponse.documents.find(doc =>
|
||||||
|
doc.user_id === user.$id && doc.month_year === currentMonth
|
||||||
|
);
|
||||||
|
|
||||||
setProfile(profileData);
|
setProfile(profileData as UserProfile);
|
||||||
setAdditionalEmails(emailsData || []);
|
setAdditionalEmails(emailsData as AdditionalEmail[]);
|
||||||
setUsage(usageData);
|
setUsage(usageData as EmailProcessingUsage);
|
||||||
|
|
||||||
// Set personal data
|
// Set personal data
|
||||||
if (userData) {
|
if (userData) {
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ import {
|
|||||||
ExternalLink,
|
ExternalLink,
|
||||||
Eye,
|
Eye,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { supabase, type Debt, type DebtVariable } from "../lib/supabase";
|
import { account, databases, functions, DATABASE_ID, COLLECTIONS, type Debt, type DebtVariable } from "../lib/appwrite";
|
||||||
|
import { ID } from "appwrite";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { formatCurrency } from "../lib/utils";
|
import { formatCurrency } from "../lib/utils";
|
||||||
import {
|
import {
|
||||||
@@ -397,17 +398,16 @@ export function DebtCard({ debt, onUpdate, debts, setDebts }: DebtCardProps) {
|
|||||||
|
|
||||||
const checkServerToken = async () => {
|
const checkServerToken = async () => {
|
||||||
try {
|
try {
|
||||||
const {
|
const user = await account.get();
|
||||||
data: { user },
|
|
||||||
} = await supabase.auth.getUser();
|
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
|
|
||||||
const { data: profile } = await supabase
|
const response = await databases.listDocuments(
|
||||||
.from("user_profiles")
|
DATABASE_ID,
|
||||||
.select("postmark_server_token")
|
COLLECTIONS.USER_PROFILES,
|
||||||
.eq("user_id", user.id)
|
[] // In production: Query.equal('user_id', user.$id)
|
||||||
.single();
|
);
|
||||||
|
|
||||||
|
const profile = response.documents.find(doc => doc.user_id === user.$id);
|
||||||
setUserProfile(profile);
|
setUserProfile(profile);
|
||||||
setHasServerToken(!!profile?.postmark_server_token);
|
setHasServerToken(!!profile?.postmark_server_token);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -427,20 +427,20 @@ export function DebtCard({ debt, onUpdate, debts, setDebts }: DebtCardProps) {
|
|||||||
|
|
||||||
setIsApproving(true);
|
setIsApproving(true);
|
||||||
try {
|
try {
|
||||||
const {
|
const user = await account.get();
|
||||||
data: { user },
|
|
||||||
} = await supabase.auth.getUser();
|
|
||||||
if (!user) throw new Error("User not authenticated");
|
if (!user) throw new Error("User not authenticated");
|
||||||
|
|
||||||
if (sendEmail) {
|
if (sendEmail) {
|
||||||
// Call the send-email function
|
// Call the send-email function
|
||||||
const { data, error } = await supabase.functions.invoke("send-email", {
|
const response = await functions.createExecution(
|
||||||
body: {
|
'send-email', // Function ID
|
||||||
|
JSON.stringify({
|
||||||
debtId: debt.id,
|
debtId: debt.id,
|
||||||
},
|
})
|
||||||
});
|
);
|
||||||
|
|
||||||
if (error) throw error;
|
const data = JSON.parse(response.response);
|
||||||
|
if (response.status === 'failed') throw new Error(data.error || 'Function execution failed');
|
||||||
|
|
||||||
if (data.requiresConfiguration) {
|
if (data.requiresConfiguration) {
|
||||||
toast.error("Configuration Required", {
|
toast.error("Configuration Required", {
|
||||||
@@ -455,17 +455,16 @@ export function DebtCard({ debt, onUpdate, debts, setDebts }: DebtCardProps) {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Call the approve-debt function to handle approval without sending email
|
// Call the approve-debt function to handle approval without sending email
|
||||||
const { data, error } = await supabase.functions.invoke(
|
const response = await functions.createExecution(
|
||||||
"approve-debt",
|
'approve-debt', // Function ID
|
||||||
{
|
JSON.stringify({
|
||||||
body: {
|
debtId: debt.id,
|
||||||
debtId: debt.id,
|
approvalNote: "Approved by user without sending email",
|
||||||
approvalNote: "Approved by user without sending email",
|
})
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (error) throw error;
|
const data = JSON.parse(response.response);
|
||||||
|
if (response.status === 'failed') throw new Error(data.error || 'Function execution failed');
|
||||||
|
|
||||||
toast.success("Debt Approved", {
|
toast.success("Debt Approved", {
|
||||||
description: `Negotiation for ${data.vendor} has been approved and saved.`,
|
description: `Negotiation for ${data.vendor} has been approved and saved.`,
|
||||||
@@ -490,9 +489,11 @@ export function DebtCard({ debt, onUpdate, debts, setDebts }: DebtCardProps) {
|
|||||||
const handleReject = async () => {
|
const handleReject = async () => {
|
||||||
setIsRejecting(true);
|
setIsRejecting(true);
|
||||||
try {
|
try {
|
||||||
const { error } = await supabase
|
await databases.updateDocument(
|
||||||
.from("debts")
|
DATABASE_ID,
|
||||||
.update({
|
COLLECTIONS.DEBTS,
|
||||||
|
debt.id,
|
||||||
|
{
|
||||||
status: "opted_out",
|
status: "opted_out",
|
||||||
metadata: {
|
metadata: {
|
||||||
...debt.metadata,
|
...debt.metadata,
|
||||||
@@ -501,20 +502,25 @@ export function DebtCard({ debt, onUpdate, debts, setDebts }: DebtCardProps) {
|
|||||||
reason: "User rejected negotiation",
|
reason: "User rejected negotiation",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
updated_at: new Date().toISOString(),
|
||||||
.eq("id", debt.id);
|
}
|
||||||
|
);
|
||||||
if (error) throw error;
|
|
||||||
|
|
||||||
// Log the action
|
// Log the action
|
||||||
await supabase.from("audit_logs").insert({
|
await databases.createDocument(
|
||||||
debt_id: debt.id,
|
DATABASE_ID,
|
||||||
action: "negotiation_rejected",
|
COLLECTIONS.AUDIT_LOGS,
|
||||||
details: {
|
ID.unique(),
|
||||||
rejectedAt: new Date().toISOString(),
|
{
|
||||||
reason: "User rejected negotiation",
|
debt_id: debt.id,
|
||||||
},
|
action: "negotiation_rejected",
|
||||||
});
|
details: {
|
||||||
|
rejectedAt: new Date().toISOString(),
|
||||||
|
reason: "User rejected negotiation",
|
||||||
|
},
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
toast.success("Negotiation Rejected", {
|
toast.success("Negotiation Rejected", {
|
||||||
description: "The negotiation has been marked as rejected.",
|
description: "The negotiation has been marked as rejected.",
|
||||||
|
|||||||
Reference in New Issue
Block a user