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
|
||||
- **Automated Negotiation**: Triggers negotiation workflows for legitimate debt collection notices
|
||||
- **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
|
||||
|
||||
Copy `.env.example` to `.env` and configure the following variables:
|
||||
|
||||
```bash
|
||||
# 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
|
||||
|
||||
# Google Generative AI API Key for Gemini model
|
||||
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
|
||||
|
||||
- `SUPABASE_URL`: Your Supabase project URL
|
||||
- `SUPABASE_ANON_KEY`: Supabase anonymous key for client-side operations
|
||||
- `SUPABASE_SERVICE_ROLE_KEY`: Supabase service role key for server-side operations (bypasses RLS)
|
||||
- `PUBLIC_APPWRITE_ENDPOINT`: Your Appwrite instance endpoint (e.g., https://cloud.appwrite.io/v1)
|
||||
- `PUBLIC_APPWRITE_PROJECT_ID`: Appwrite project ID
|
||||
- `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
|
||||
|
||||
## 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
|
||||
|
||||
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
|
||||
2. Processes opt-out requests
|
||||
3. Uses AI to extract debt information
|
||||
4. Stores processed data in Supabase
|
||||
4. Stores processed data in Appwrite
|
||||
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
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
npm install
|
||||
|
||||
# Start development server
|
||||
pnpm dev
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## 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 {
|
||||
supabase,
|
||||
account,
|
||||
databases,
|
||||
DATABASE_ID,
|
||||
COLLECTIONS,
|
||||
type AdditionalEmail,
|
||||
type UserProfile,
|
||||
type EmailProcessingUsage,
|
||||
} from "../lib/supabase";
|
||||
} from "../lib/appwrite";
|
||||
import { ID } from "appwrite";
|
||||
import { Button } from "./ui/button";
|
||||
import { Input } from "./ui/input";
|
||||
import { Label } from "./ui/label";
|
||||
@@ -65,44 +69,48 @@ export function Configuration() {
|
||||
|
||||
const fetchUserData = async () => {
|
||||
try {
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser();
|
||||
const user = await account.get();
|
||||
if (!user) return;
|
||||
|
||||
// Fetch user profile
|
||||
const { data: profileData } = await supabase
|
||||
.from("user_profiles")
|
||||
.select("*")
|
||||
.eq("user_id", user.id)
|
||||
.single();
|
||||
const profileResponse = await databases.listDocuments(
|
||||
DATABASE_ID,
|
||||
COLLECTIONS.USER_PROFILES,
|
||||
[] // In production: Query.equal('user_id', user.$id)
|
||||
);
|
||||
const profileData = profileResponse.documents.find(doc => doc.user_id === user.$id);
|
||||
|
||||
// Fetch user personal data
|
||||
const { data: userData } = await supabase
|
||||
.from("users")
|
||||
.select("*")
|
||||
.eq("id", user.id)
|
||||
.single();
|
||||
// Fetch user personal data from users collection
|
||||
const usersResponse = await databases.listDocuments(
|
||||
DATABASE_ID,
|
||||
'users', // Assuming users collection exists
|
||||
[] // In production: Query.equal('id', user.$id)
|
||||
);
|
||||
const userData = usersResponse.documents.find(doc => doc.id === user.$id);
|
||||
|
||||
// Fetch additional emails
|
||||
const { data: emailsData } = await supabase
|
||||
.from("additional_emails")
|
||||
.select("*")
|
||||
.eq("user_id", user.id)
|
||||
.order("created_at", { ascending: false });
|
||||
const emailsResponse = await databases.listDocuments(
|
||||
DATABASE_ID,
|
||||
COLLECTIONS.ADDITIONAL_EMAILS,
|
||||
[] // In production: Query.equal('user_id', user.$id), Query.orderDesc('created_at')
|
||||
);
|
||||
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
|
||||
const currentMonth = new Date().toISOString().slice(0, 7); // YYYY-MM
|
||||
const { data: usageData } = await supabase
|
||||
.from("email_processing_usage")
|
||||
.select("*")
|
||||
.eq("user_id", user.id)
|
||||
.eq("month_year", currentMonth)
|
||||
.single();
|
||||
const usageResponse = await databases.listDocuments(
|
||||
DATABASE_ID,
|
||||
COLLECTIONS.EMAIL_PROCESSING_USAGE,
|
||||
[] // In production: Query.equal('user_id', user.$id), Query.equal('month_year', currentMonth)
|
||||
);
|
||||
const usageData = usageResponse.documents.find(doc =>
|
||||
doc.user_id === user.$id && doc.month_year === currentMonth
|
||||
);
|
||||
|
||||
setProfile(profileData);
|
||||
setAdditionalEmails(emailsData || []);
|
||||
setUsage(usageData);
|
||||
setProfile(profileData as UserProfile);
|
||||
setAdditionalEmails(emailsData as AdditionalEmail[]);
|
||||
setUsage(usageData as EmailProcessingUsage);
|
||||
|
||||
// Set personal data
|
||||
if (userData) {
|
||||
|
||||
@@ -44,7 +44,8 @@ import {
|
||||
ExternalLink,
|
||||
Eye,
|
||||
} 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 { formatCurrency } from "../lib/utils";
|
||||
import {
|
||||
@@ -397,17 +398,16 @@ export function DebtCard({ debt, onUpdate, debts, setDebts }: DebtCardProps) {
|
||||
|
||||
const checkServerToken = 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("postmark_server_token")
|
||||
.eq("user_id", user.id)
|
||||
.single();
|
||||
const response = await databases.listDocuments(
|
||||
DATABASE_ID,
|
||||
COLLECTIONS.USER_PROFILES,
|
||||
[] // In production: Query.equal('user_id', user.$id)
|
||||
);
|
||||
|
||||
const profile = response.documents.find(doc => doc.user_id === user.$id);
|
||||
setUserProfile(profile);
|
||||
setHasServerToken(!!profile?.postmark_server_token);
|
||||
} catch (error) {
|
||||
@@ -427,20 +427,20 @@ export function DebtCard({ debt, onUpdate, debts, setDebts }: DebtCardProps) {
|
||||
|
||||
setIsApproving(true);
|
||||
try {
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser();
|
||||
const user = await account.get();
|
||||
if (!user) throw new Error("User not authenticated");
|
||||
|
||||
if (sendEmail) {
|
||||
// Call the send-email function
|
||||
const { data, error } = await supabase.functions.invoke("send-email", {
|
||||
body: {
|
||||
const response = await functions.createExecution(
|
||||
'send-email', // Function ID
|
||||
JSON.stringify({
|
||||
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) {
|
||||
toast.error("Configuration Required", {
|
||||
@@ -455,17 +455,16 @@ export function DebtCard({ debt, onUpdate, debts, setDebts }: DebtCardProps) {
|
||||
});
|
||||
} else {
|
||||
// Call the approve-debt function to handle approval without sending email
|
||||
const { data, error } = await supabase.functions.invoke(
|
||||
"approve-debt",
|
||||
{
|
||||
body: {
|
||||
debtId: debt.id,
|
||||
approvalNote: "Approved by user without sending email",
|
||||
},
|
||||
}
|
||||
const response = await functions.createExecution(
|
||||
'approve-debt', // Function ID
|
||||
JSON.stringify({
|
||||
debtId: debt.id,
|
||||
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", {
|
||||
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 () => {
|
||||
setIsRejecting(true);
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from("debts")
|
||||
.update({
|
||||
await databases.updateDocument(
|
||||
DATABASE_ID,
|
||||
COLLECTIONS.DEBTS,
|
||||
debt.id,
|
||||
{
|
||||
status: "opted_out",
|
||||
metadata: {
|
||||
...debt.metadata,
|
||||
@@ -501,20 +502,25 @@ export function DebtCard({ debt, onUpdate, debts, setDebts }: DebtCardProps) {
|
||||
reason: "User rejected negotiation",
|
||||
},
|
||||
},
|
||||
})
|
||||
.eq("id", debt.id);
|
||||
|
||||
if (error) throw error;
|
||||
updated_at: new Date().toISOString(),
|
||||
}
|
||||
);
|
||||
|
||||
// Log the action
|
||||
await supabase.from("audit_logs").insert({
|
||||
debt_id: debt.id,
|
||||
action: "negotiation_rejected",
|
||||
details: {
|
||||
rejectedAt: new Date().toISOString(),
|
||||
reason: "User rejected negotiation",
|
||||
},
|
||||
});
|
||||
await databases.createDocument(
|
||||
DATABASE_ID,
|
||||
COLLECTIONS.AUDIT_LOGS,
|
||||
ID.unique(),
|
||||
{
|
||||
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", {
|
||||
description: "The negotiation has been marked as rejected.",
|
||||
|
||||
Reference in New Issue
Block a user