Merge pull request #1 from FranP-code/copilot/fix-c03dd869-9081-4f5e-a7cf-c61635f87fb6

This commit is contained in:
Francisco Pessano
2025-09-01 15:58:27 -03:00
committed by GitHub
21 changed files with 1455 additions and 700 deletions

View File

@@ -1,13 +1,19 @@
# Environment variables for Inbox Negotiator # Environment variables for Inbox Negotiator
# 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
# 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 for Gemini model
GOOGLE_GENERATIVE_AI_API_KEY=your_google_api_key_here GOOGLE_GENERATIVE_AI_API_KEY=your_google_api_key_here
# Add these same variables to your actual .env file # Add these same variables to your actual .env file
# Note: The SUPABASE_SERVICE_ROLE_KEY is required for webhook operations # Note: The APPWRITE_API_KEY is required for server-side operations
# to bypass Row Level Security (RLS) policies in server-side contexts # and webhook operations with admin privileges

View File

@@ -0,0 +1,60 @@
name: Deploy Appwrite Functions
on:
push:
branches: [main]
paths:
- 'appwrite/functions/**'
- 'supabase/functions/**'
workflow_dispatch:
jobs:
deploy-functions:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: latest
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Install Appwrite CLI
run: npm install -g appwrite-cli
- name: Setup Appwrite CLI
run: |
appwrite client \
--endpoint ${{ secrets.APPWRITE_ENDPOINT }} \
--project-id ${{ secrets.APPWRITE_PROJECT_ID }} \
--key ${{ secrets.APPWRITE_API_KEY }}
- name: Deploy Functions
run: ./scripts/deploy-appwrite-functions.sh
env:
APPWRITE_PROJECT_ID: ${{ secrets.APPWRITE_PROJECT_ID }}
APPWRITE_API_KEY: ${{ secrets.APPWRITE_API_KEY }}
APPWRITE_ENDPOINT: ${{ secrets.APPWRITE_ENDPOINT }}
GOOGLE_AI_API_KEY: ${{ secrets.GOOGLE_AI_API_KEY }}
POSTMARK_SERVER_TOKEN: ${{ secrets.POSTMARK_SERVER_TOKEN }}
- name: Create deployment summary
run: |
echo "## 🚀 Function Deployment Summary" >> $GITHUB_STEP_SUMMARY
echo "Deployed at: $(date)" >> $GITHUB_STEP_SUMMARY
echo "### Functions deployed:" >> $GITHUB_STEP_SUMMARY
for func in appwrite/functions/*/; do
if [ -d "$func" ]; then
echo "- $(basename "$func")" >> $GITHUB_STEP_SUMMARY
fi
done

3
.npmrc Normal file
View File

@@ -0,0 +1,3 @@
# Use pnpm as the package manager for this project
# This ensures consistent dependency management across all environments
package-manager=pnpm

154
APPWRITE_MIGRATION.md Normal file
View File

@@ -0,0 +1,154 @@
# 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
```
### Auto-Deploy Functions Setup
#### 1. Install Appwrite CLI
```bash
npm install -g appwrite-cli
```
#### 2. Initialize Appwrite Project
```bash
pnpm run setup:appwrite
# or manually: appwrite init project --project-id your_project_id
```
#### 3. Deploy Functions Automatically
```bash
# Deploy all functions to Appwrite
pnpm run deploy:functions
# Or run the script directly
./scripts/deploy-appwrite-functions.sh
```
#### 4. GitHub Actions Auto-Deploy
The repository includes a GitHub Actions workflow (`.github/workflows/deploy-appwrite-functions.yml`) that automatically deploys functions when:
- Changes are pushed to the `main` branch
- Files in `appwrite/functions/` or `supabase/functions/` are modified
- Manually triggered via workflow dispatch
**Required GitHub Secrets:**
- `APPWRITE_PROJECT_ID` - Your Appwrite project ID
- `APPWRITE_API_KEY` - Your Appwrite API key with Functions write permissions
- `APPWRITE_ENDPOINT` - Your Appwrite endpoint (default: https://cloud.appwrite.io/v1)
- `GOOGLE_AI_API_KEY` - For AI-powered functions
- `POSTMARK_SERVER_TOKEN` - For email functions
### 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

View File

@@ -7,17 +7,48 @@ 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
## Development Setup
This project uses **pnpm** as the package manager. Make sure you have pnpm installed:
```bash
npm install -g pnpm
```
### Installation
```bash
# Clone the repository
git clone <repository-url>
cd inbox-negotiator
# Install dependencies
pnpm install
# Start development server
pnpm run dev
```
### Available Scripts
- `pnpm run dev` - Start development server
- `pnpm run build` - Build for production
- `pnpm run preview` - Preview production build
- `pnpm run deploy:functions` - Deploy Appwrite functions
- `pnpm run setup:appwrite` - Initialize Appwrite project
## 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 +56,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 +80,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.

85
appwrite/appwrite.json Normal file
View File

@@ -0,0 +1,85 @@
{
"projectId": "$APPWRITE_PROJECT_ID",
"projectName": "InboxNegotiator",
"functions": [
{
"name": "negotiate",
"functionId": "negotiate",
"runtime": "node-18.0",
"entrypoint": "src/main.js",
"commands": "npm install",
"timeout": 15,
"enabled": true,
"execute": ["any"],
"events": [],
"schedule": "",
"env": {
"GOOGLE_AI_API_KEY": "$GOOGLE_AI_API_KEY",
"APPWRITE_DATABASE_ID": "$APPWRITE_DATABASE_ID"
}
},
{
"name": "approve-debt",
"functionId": "approve-debt",
"runtime": "node-18.0",
"entrypoint": "src/main.js",
"commands": "npm install",
"timeout": 15,
"enabled": true,
"execute": ["any"],
"events": [],
"schedule": "",
"env": {
"APPWRITE_DATABASE_ID": "$APPWRITE_DATABASE_ID"
}
},
{
"name": "send-email",
"functionId": "send-email",
"runtime": "node-18.0",
"entrypoint": "src/main.js",
"commands": "npm install",
"timeout": 15,
"enabled": true,
"execute": ["any"],
"events": [],
"schedule": "",
"env": {
"POSTMARK_SERVER_TOKEN": "$POSTMARK_SERVER_TOKEN",
"APPWRITE_DATABASE_ID": "$APPWRITE_DATABASE_ID"
}
},
{
"name": "analyze-response",
"functionId": "analyze-response",
"runtime": "node-18.0",
"entrypoint": "src/main.js",
"commands": "npm install",
"timeout": 15,
"enabled": true,
"execute": ["any"],
"events": [],
"schedule": "",
"env": {
"GOOGLE_AI_API_KEY": "$GOOGLE_AI_API_KEY",
"APPWRITE_DATABASE_ID": "$APPWRITE_DATABASE_ID"
}
},
{
"name": "test-extraction",
"functionId": "test-extraction",
"runtime": "node-18.0",
"entrypoint": "src/main.js",
"commands": "npm install",
"timeout": 15,
"enabled": true,
"execute": ["any"],
"events": [],
"schedule": "",
"env": {
"GOOGLE_AI_API_KEY": "$GOOGLE_AI_API_KEY",
"APPWRITE_DATABASE_ID": "$APPWRITE_DATABASE_ID"
}
}
]
}

View File

@@ -8,7 +8,9 @@
"start": "astro dev", "start": "astro dev",
"build": "astro build", "build": "astro build",
"preview": "astro preview", "preview": "astro preview",
"astro": "astro" "astro": "astro",
"deploy:functions": "./scripts/deploy-appwrite-functions.sh",
"setup:appwrite": "appwrite init project"
}, },
"dependencies": { "dependencies": {
"@ai-sdk/google": "^1.2.19", "@ai-sdk/google": "^1.2.19",
@@ -44,11 +46,11 @@
"@radix-ui/react-toggle": "^1.1.0", "@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0", "@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.2",
"@supabase/supabase-js": "^2.50.0",
"@types/react": "^18.3.10", "@types/react": "^18.3.10",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"@vercel/analytics": "^1.5.0", "@vercel/analytics": "^1.5.0",
"ai": "^4.3.16", "ai": "^4.3.16",
"appwrite": "^18.2.0",
"astro": "^5.9.0", "astro": "^5.9.0",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",

101
pnpm-lock.yaml generated
View File

@@ -107,9 +107,6 @@ importers:
'@radix-ui/react-tooltip': '@radix-ui/react-tooltip':
specifier: ^1.1.2 specifier: ^1.1.2
version: 1.2.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 1.2.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@supabase/supabase-js':
specifier: ^2.50.0
version: 2.50.0
'@types/react': '@types/react':
specifier: ^18.3.10 specifier: ^18.3.10
version: 18.3.23 version: 18.3.23
@@ -122,6 +119,9 @@ importers:
ai: ai:
specifier: ^4.3.16 specifier: ^4.3.16
version: 4.3.16(react@18.3.1)(zod@3.23.8) version: 4.3.16(react@18.3.1)(zod@3.23.8)
appwrite:
specifier: ^18.2.0
version: 18.2.0
astro: astro:
specifier: ^5.9.0 specifier: ^5.9.0
version: 5.9.0(@types/node@22.15.30)(jiti@1.21.7)(rollup@4.42.0)(typescript@5.8.3)(yaml@2.8.0) version: 5.9.0(@types/node@22.15.30)(jiti@1.21.7)(rollup@4.42.0)(typescript@5.8.3)(yaml@2.8.0)
@@ -1449,28 +1449,6 @@ packages:
'@shikijs/vscode-textmate@10.0.2': '@shikijs/vscode-textmate@10.0.2':
resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
'@supabase/auth-js@2.70.0':
resolution: {integrity: sha512-BaAK/tOAZFJtzF1sE3gJ2FwTjLf4ky3PSvcvLGEgEmO4BSBkwWKu8l67rLLIBZPDnCyV7Owk2uPyKHa0kj5QGg==}
'@supabase/functions-js@2.4.4':
resolution: {integrity: sha512-WL2p6r4AXNGwop7iwvul2BvOtuJ1YQy8EbOd0dhG1oN1q8el/BIRSFCFnWAMM/vJJlHWLi4ad22sKbKr9mvjoA==}
'@supabase/node-fetch@2.6.15':
resolution: {integrity: sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==}
engines: {node: 4.x || >=6.0.0}
'@supabase/postgrest-js@1.19.4':
resolution: {integrity: sha512-O4soKqKtZIW3olqmbXXbKugUtByD2jPa8kL2m2c1oozAO11uCcGrRhkZL0kVxjBLrXHE0mdSkFsMj7jDSfyNpw==}
'@supabase/realtime-js@2.11.10':
resolution: {integrity: sha512-SJKVa7EejnuyfImrbzx+HaD9i6T784khuw1zP+MBD7BmJYChegGxYigPzkKX8CK8nGuDntmeSD3fvriaH0EGZA==}
'@supabase/storage-js@2.7.1':
resolution: {integrity: sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA==}
'@supabase/supabase-js@2.50.0':
resolution: {integrity: sha512-M1Gd5tPaaghYZ9OjeO1iORRqbTWFEz/cF3pPubRnMPzA+A8SiUsXXWDP+DWsASZcjEcVEcVQIAF38i5wrijYOg==}
'@swc/helpers@0.5.17': '@swc/helpers@0.5.17':
resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==}
@@ -1543,9 +1521,6 @@ packages:
'@types/node@22.15.30': '@types/node@22.15.30':
resolution: {integrity: sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA==} resolution: {integrity: sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA==}
'@types/phoenix@1.6.6':
resolution: {integrity: sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==}
'@types/prop-types@15.7.14': '@types/prop-types@15.7.14':
resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==} resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==}
@@ -1560,9 +1535,6 @@ packages:
'@types/unist@3.0.3': '@types/unist@3.0.3':
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
'@types/ws@8.18.1':
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
'@ungap/structured-clone@1.3.0': '@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
@@ -1666,6 +1638,9 @@ packages:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
appwrite@18.2.0:
resolution: {integrity: sha512-g7pQpsxqR7+amEIaQLXMN4XzdQKenTHnGdA4s7UUJdZufhlHdJby8895h8z893+S0XipeHZhi0wpxYA2An95Rg==}
arg@5.0.2: arg@5.0.2:
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
@@ -3343,18 +3318,6 @@ packages:
resolution: {integrity: sha512-GmqrO8WJ1NuzJ2DrziEI2o57jKAVIQNf8a18W3nCYU3H7PNWqCCVTeH6/NQE93CIllIgQS98rrmVkYgTX9fFJQ==} resolution: {integrity: sha512-GmqrO8WJ1NuzJ2DrziEI2o57jKAVIQNf8a18W3nCYU3H7PNWqCCVTeH6/NQE93CIllIgQS98rrmVkYgTX9fFJQ==}
engines: {node: ^18.17.0 || >=20.5.0} engines: {node: ^18.17.0 || >=20.5.0}
ws@8.18.2:
resolution: {integrity: sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: '>=5.0.2'
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
xxhash-wasm@1.1.0: xxhash-wasm@1.1.0:
resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==} resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==}
@@ -4686,48 +4649,6 @@ snapshots:
'@shikijs/vscode-textmate@10.0.2': {} '@shikijs/vscode-textmate@10.0.2': {}
'@supabase/auth-js@2.70.0':
dependencies:
'@supabase/node-fetch': 2.6.15
'@supabase/functions-js@2.4.4':
dependencies:
'@supabase/node-fetch': 2.6.15
'@supabase/node-fetch@2.6.15':
dependencies:
whatwg-url: 5.0.0
'@supabase/postgrest-js@1.19.4':
dependencies:
'@supabase/node-fetch': 2.6.15
'@supabase/realtime-js@2.11.10':
dependencies:
'@supabase/node-fetch': 2.6.15
'@types/phoenix': 1.6.6
'@types/ws': 8.18.1
ws: 8.18.2
transitivePeerDependencies:
- bufferutil
- utf-8-validate
'@supabase/storage-js@2.7.1':
dependencies:
'@supabase/node-fetch': 2.6.15
'@supabase/supabase-js@2.50.0':
dependencies:
'@supabase/auth-js': 2.70.0
'@supabase/functions-js': 2.4.4
'@supabase/node-fetch': 2.6.15
'@supabase/postgrest-js': 1.19.4
'@supabase/realtime-js': 2.11.10
'@supabase/storage-js': 2.7.1
transitivePeerDependencies:
- bufferutil
- utf-8-validate
'@swc/helpers@0.5.17': '@swc/helpers@0.5.17':
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
@@ -4809,8 +4730,6 @@ snapshots:
dependencies: dependencies:
undici-types: 6.21.0 undici-types: 6.21.0
'@types/phoenix@1.6.6': {}
'@types/prop-types@15.7.14': {} '@types/prop-types@15.7.14': {}
'@types/react-dom@18.3.7(@types/react@18.3.23)': '@types/react-dom@18.3.7(@types/react@18.3.23)':
@@ -4824,10 +4743,6 @@ snapshots:
'@types/unist@3.0.3': {} '@types/unist@3.0.3': {}
'@types/ws@8.18.1':
dependencies:
'@types/node': 22.15.30
'@ungap/structured-clone@1.3.0': {} '@ungap/structured-clone@1.3.0': {}
'@vercel/analytics@1.5.0(react@18.3.1)': '@vercel/analytics@1.5.0(react@18.3.1)':
@@ -4925,6 +4840,8 @@ snapshots:
normalize-path: 3.0.0 normalize-path: 3.0.0
picomatch: 2.3.1 picomatch: 2.3.1
appwrite@18.2.0: {}
arg@5.0.2: {} arg@5.0.2: {}
argparse@2.0.1: {} argparse@2.0.1: {}
@@ -6915,8 +6832,6 @@ snapshots:
imurmurhash: 0.1.4 imurmurhash: 0.1.4
signal-exit: 4.1.0 signal-exit: 4.1.0
ws@8.18.2: {}
xxhash-wasm@1.1.0: {} xxhash-wasm@1.1.0: {}
yallist@3.1.1: {} yallist@3.1.1: {}

View File

@@ -0,0 +1,222 @@
#!/bin/bash
# Appwrite Functions Auto-Deploy Script
# This script migrates Supabase Edge Functions to Appwrite Functions
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Configuration
SUPABASE_FUNCTIONS_DIR="./supabase/functions"
APPWRITE_FUNCTIONS_DIR="./appwrite/functions"
APPWRITE_CONFIG="./appwrite/appwrite.json"
echo -e "${GREEN}🚀 Appwrite Functions Auto-Deploy Script${NC}"
echo "========================================"
# Check if Appwrite CLI is installed
if ! command -v appwrite &> /dev/null; then
echo -e "${RED}❌ Appwrite CLI not found${NC}"
echo "Please install it with: npm install -g appwrite-cli"
exit 1
fi
# Check environment variables
if [ -z "$APPWRITE_PROJECT_ID" ]; then
echo -e "${RED}❌ APPWRITE_PROJECT_ID environment variable not set${NC}"
exit 1
fi
if [ -z "$APPWRITE_API_KEY" ]; then
echo -e "${RED}❌ APPWRITE_API_KEY environment variable not set${NC}"
exit 1
fi
echo -e "${YELLOW}📋 Environment Variables:${NC}"
echo "APPWRITE_PROJECT_ID: $APPWRITE_PROJECT_ID"
echo "APPWRITE_API_KEY: ${APPWRITE_API_KEY:0:8}..."
echo ""
# Initialize Appwrite project if not already done
echo -e "${YELLOW}🔧 Setting up Appwrite project...${NC}"
if [ ! -f ".appwrite/project.json" ]; then
appwrite init project --project-id "$APPWRITE_PROJECT_ID"
fi
# Create Appwrite functions directory if it doesn't exist
mkdir -p "$APPWRITE_FUNCTIONS_DIR"
# Function to convert Supabase function to Appwrite function
convert_function() {
local func_name=$1
local supabase_func_dir="$SUPABASE_FUNCTIONS_DIR/$func_name"
local appwrite_func_dir="$APPWRITE_FUNCTIONS_DIR/$func_name"
if [ ! -d "$supabase_func_dir" ]; then
echo -e "${RED}❌ Supabase function '$func_name' not found${NC}"
return 1
fi
echo -e "${YELLOW}🔄 Converting function: $func_name${NC}"
# Create Appwrite function directory
mkdir -p "$appwrite_func_dir/src"
# Copy and convert the main file
if [ -f "$supabase_func_dir/index.ts" ]; then
# Convert Supabase function to Appwrite function format
cat > "$appwrite_func_dir/src/main.js" << 'EOF'
import { Client, Databases, Functions } from 'node-appwrite';
// This is a migrated function from Supabase to Appwrite
// Original Supabase function logic should be adapted here
export default async ({ req, res, log, error }) => {
const client = new Client()
.setEndpoint(process.env.APPWRITE_FUNCTION_ENDPOINT)
.setProject(process.env.APPWRITE_FUNCTION_PROJECT_ID)
.setKey(process.env.APPWRITE_API_KEY);
const databases = new Databases(client);
try {
// TODO: Migrate Supabase function logic here
// Original file: ${supabase_func_dir}/index.ts
log('Function executed successfully');
return res.json({ success: true, message: 'Function migrated from Supabase' });
} catch (err) {
error('Function execution failed: ' + err.message);
return res.json({ success: false, error: err.message }, 500);
}
};
EOF
# Create package.json for the function
cat > "$appwrite_func_dir/package.json" << EOF
{
"name": "$func_name",
"version": "1.0.0",
"description": "Migrated from Supabase Edge Function",
"main": "src/main.js",
"type": "module",
"dependencies": {
"node-appwrite": "^13.0.0"
}
}
EOF
# Create README with migration notes
cat > "$appwrite_func_dir/README.md" << EOF
# $func_name Function
This function was migrated from Supabase Edge Functions to Appwrite Functions.
## Original Supabase Function
- Location: \`$supabase_func_dir/index.ts\`
- Migrated on: $(date)
## Migration Notes
- The function logic needs to be manually adapted from the original Supabase function
- Update environment variables and dependencies as needed
- Test thoroughly before deploying to production
## Deployment
\`\`\`bash
appwrite functions createDeployment --function-id=$func_name --activate=true
\`\`\`
EOF
echo -e "${GREEN}✅ Function '$func_name' converted successfully${NC}"
else
echo -e "${RED}❌ No index.ts found for function '$func_name'${NC}"
return 1
fi
}
# Deploy function to Appwrite
deploy_function() {
local func_name=$1
local appwrite_func_dir="$APPWRITE_FUNCTIONS_DIR/$func_name"
if [ ! -d "$appwrite_func_dir" ]; then
echo -e "${RED}❌ Appwrite function '$func_name' not found${NC}"
return 1
fi
echo -e "${YELLOW}🚀 Deploying function: $func_name${NC}"
# Create function if it doesn't exist
appwrite functions create \
--function-id="$func_name" \
--name="$func_name" \
--runtime="node-18.0" \
--execute='["any"]' \
--timeout=15 \
--enabled=true || echo "Function may already exist, continuing..."
# Deploy the function
cd "$appwrite_func_dir"
appwrite functions createDeployment \
--function-id="$func_name" \
--activate=true \
--code="."
cd - > /dev/null
echo -e "${GREEN}✅ Function '$func_name' deployed successfully${NC}"
}
# Main execution
echo -e "${YELLOW}📋 Available Supabase functions:${NC}"
for func_dir in "$SUPABASE_FUNCTIONS_DIR"/*; do
if [ -d "$func_dir" ]; then
func_name=$(basename "$func_dir")
echo " - $func_name"
fi
done
echo ""
# Convert functions
echo -e "${YELLOW}🔄 Converting Supabase functions to Appwrite format...${NC}"
for func_dir in "$SUPABASE_FUNCTIONS_DIR"/*; do
if [ -d "$func_dir" ]; then
func_name=$(basename "$func_dir")
convert_function "$func_name"
fi
done
echo ""
echo -e "${YELLOW}🚀 Deploying functions to Appwrite...${NC}"
# Ask for confirmation
read -p "Do you want to deploy all functions to Appwrite? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
for func_dir in "$APPWRITE_FUNCTIONS_DIR"/*; do
if [ -d "$func_dir" ]; then
func_name=$(basename "$func_dir")
deploy_function "$func_name"
fi
done
echo ""
echo -e "${GREEN}🎉 All functions deployed successfully!${NC}"
echo ""
echo -e "${YELLOW}📝 Next steps:${NC}"
echo "1. Update each function's logic in appwrite/functions/*/src/main.js"
echo "2. Test functions in Appwrite console"
echo "3. Update your application to use Appwrite function IDs"
echo "4. Set up environment variables for each function"
else
echo -e "${YELLOW}⏭️ Function deployment skipped${NC}"
echo "You can deploy individual functions later using:"
echo " appwrite functions createDeployment --function-id=FUNCTION_NAME --activate=true"
fi
echo ""
echo -e "${GREEN}✨ Migration process completed!${NC}"

View File

@@ -1,11 +1,12 @@
import React, { useState } from 'react'; 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 { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Alert, AlertDescription } from '@/components/ui/alert'; import { Alert, AlertDescription } from '@/components/ui/alert';
import { Loader2, Mail, Lock, User } from 'lucide-react'; import { Loader2, Mail, Lock, User } from 'lucide-react';
import { ID } from 'appwrite';
interface AuthFormProps { interface AuthFormProps {
mode: 'login' | 'signup'; mode: 'login' | 'signup';
@@ -27,32 +28,28 @@ export function AuthForm({ mode }: AuthFormProps) {
try { try {
if (mode === 'signup') { if (mode === 'signup') {
const { error } = await supabase.auth.signUp({ // Create account with Appwrite
await account.create(
ID.unique(),
email, email,
password, password,
options: { fullName
data: { );
full_name: fullName,
}
}
});
if (error) throw error; // Create session after account creation
setMessage('Check your email for the confirmation link!'); await account.createEmailPasswordSession(email, password);
setMessage('Account created successfully!');
window.location.href = '/dashboard'; window.location.href = '/dashboard';
} else { } else {
const { error } = await supabase.auth.signInWithPassword({ // Sign in with Appwrite
email, await account.createEmailPasswordSession(email, password);
password,
});
if (error) throw error;
// Redirect to dashboard on successful login // Redirect to dashboard on successful login
window.location.href = '/dashboard'; window.location.href = '/dashboard';
} }
} catch (error: any) { } catch (error: any) {
setError(error.message); setError(error.message || 'An error occurred');
} finally { } finally {
setLoading(false); setLoading(false);
} }

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { supabase } from '../lib/supabase'; import { account } from '../lib/appwrite';
import type { User } from '@supabase/supabase-js'; import type { Models } from 'appwrite';
import { Loader2 } from 'lucide-react'; import { Loader2 } from 'lucide-react';
interface AuthGuardProps { interface AuthGuardProps {
@@ -9,47 +9,38 @@ interface AuthGuardProps {
} }
export function AuthGuard({ children, requireAuth = true }: 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); const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
// Get initial session // Get initial session
supabase.auth.getSession().then(({ data: { session } }) => { account.get().then((currentUser) => {
setUser(session?.user ?? null); setUser(currentUser);
setLoading(false); setLoading(false);
// Redirect logic // Redirect logic
if (requireAuth && !session?.user) { if (requireAuth && !currentUser) {
// User needs to be authenticated but isn't - redirect to login // User needs to be authenticated but isn't - redirect to login
window.location.href = '/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 // User is authenticated but on a public page - redirect to dashboard
const currentPath = window.location.pathname; const currentPath = window.location.pathname;
if (currentPath === '/login' || currentPath === '/signup') { if (currentPath === '/login' || currentPath === '/signup') {
window.location.href = '/dashboard'; window.location.href = '/dashboard';
} }
} }
}).catch(() => {
// No user session found
setUser(null);
setLoading(false);
if (requireAuth) {
window.location.href = '/login';
}
}); });
// Listen for auth changes // Note: Appwrite doesn't have built-in session listeners like Supabase
const { data: { subscription } } = supabase.auth.onAuthStateChange( // You might need to implement session checking through other means or use Appwrite's real-time features
(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();
}, [requireAuth]); }, [requireAuth]);
if (loading) { if (loading) {

View File

@@ -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, Query } 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,45 @@ 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) [Query.equal('user_id', user.$id)]
.single(); );
const profileData = profileResponse.documents[0];
// 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) [Query.equal('id', user.$id)]
.single(); );
const userData = usersResponse.documents[0];
// 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) [Query.equal('user_id', user.$id), Query.orderDesc('created_at')]
.order("created_at", { ascending: false }); );
const emailsData = emailsResponse.documents;
// 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) [Query.equal('user_id', user.$id), Query.equal('month_year', currentMonth)]
.eq("month_year", currentMonth) );
.single(); const usageData = usageResponse.documents[0];
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) {
@@ -131,25 +136,46 @@ export function Configuration() {
const savePersonalData = async () => { const savePersonalData = async () => {
setSavingPersonalData(true); setSavingPersonalData(true);
try { try {
const { const user = await account.get();
data: { user },
} = await supabase.auth.getUser();
if (!user) return; if (!user) return;
const { error } = await supabase // First, try to get the existing user document
.from("users") const usersResponse = await databases.listDocuments(
.update({ DATABASE_ID,
full_name: personalData.full_name || null, 'users',
address_line_1: personalData.address_line_1 || null, [Query.equal('id', user.$id)]
address_line_2: personalData.address_line_2 || null, );
city: personalData.city || null,
state: personalData.state || null,
zip_code: personalData.zip_code || null,
phone_number: personalData.phone_number || null,
})
.eq("id", user.id);
if (error) throw error; const updateData = {
full_name: personalData.full_name || null,
address_line_1: personalData.address_line_1 || null,
address_line_2: personalData.address_line_2 || null,
city: personalData.city || null,
state: personalData.state || null,
zip_code: personalData.zip_code || null,
phone_number: personalData.phone_number || null,
};
if (usersResponse.documents.length > 0) {
// Update existing document
await databases.updateDocument(
DATABASE_ID,
'users',
usersResponse.documents[0].$id,
updateData
);
} else {
// Create new document if it doesn't exist
await databases.createDocument(
DATABASE_ID,
'users',
ID.unique(),
{
id: user.$id,
...updateData
}
);
}
toast.success("Personal data updated", { toast.success("Personal data updated", {
description: "Your personal information has been saved successfully.", description: "Your personal information has been saved successfully.",
@@ -166,19 +192,42 @@ export function Configuration() {
const saveServerToken = async () => { const saveServerToken = async () => {
setSavingServerToken(true); setSavingServerToken(true);
try { try {
const { const user = await account.get();
data: { user },
} = await supabase.auth.getUser();
if (!user) return; if (!user) return;
const { error } = await supabase // Get the existing user profile document
.from("user_profiles") const profileResponse = await databases.listDocuments(
.update({ DATABASE_ID,
postmark_server_token: serverToken || null, COLLECTIONS.USER_PROFILES,
}) [Query.equal('user_id', user.$id)]
.eq("user_id", user.id); );
if (error) throw error; if (profileResponse.documents.length > 0) {
// Update existing profile
await databases.updateDocument(
DATABASE_ID,
COLLECTIONS.USER_PROFILES,
profileResponse.documents[0].$id,
{
postmark_server_token: serverToken || null,
}
);
} else {
// Create new profile if it doesn't exist
await databases.createDocument(
DATABASE_ID,
COLLECTIONS.USER_PROFILES,
ID.unique(),
{
user_id: user.$id,
postmark_server_token: serverToken || null,
onboarding_completed: false,
email_processing_limit: 1000,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
}
);
}
// Update local profile state // Update local profile state
setProfile((prev) => setProfile((prev) =>
@@ -202,23 +251,24 @@ export function Configuration() {
setAddingEmail(true); setAddingEmail(true);
try { try {
const { const user = await account.get();
data: { user },
} = await supabase.auth.getUser();
if (!user) return; if (!user) return;
const { data, error } = await supabase const newEmailDoc = await databases.createDocument(
.from("additional_emails") DATABASE_ID,
.insert({ COLLECTIONS.ADDITIONAL_EMAILS,
user_id: user.id, ID.unique(),
{
user_id: user.$id,
email_address: newEmail.trim().toLowerCase(), email_address: newEmail.trim().toLowerCase(),
}) verified: false,
.select() verification_token: null,
.single(); created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
}
);
if (error) throw error; setAdditionalEmails([newEmailDoc as AdditionalEmail, ...additionalEmails]);
setAdditionalEmails([data, ...additionalEmails]);
setNewEmail(""); setNewEmail("");
toast.success("Email added successfully", { toast.success("Email added successfully", {
description: "Additional email has been added to your account.", description: "Additional email has been added to your account.",
@@ -234,15 +284,14 @@ export function Configuration() {
const removeAdditionalEmail = async (emailId: string) => { const removeAdditionalEmail = async (emailId: string) => {
try { try {
const { error } = await supabase await databases.deleteDocument(
.from("additional_emails") DATABASE_ID,
.delete() COLLECTIONS.ADDITIONAL_EMAILS,
.eq("id", emailId); emailId
);
if (error) throw error;
setAdditionalEmails( setAdditionalEmails(
additionalEmails.filter((email) => email.id !== emailId) additionalEmails.filter((email) => email.$id !== emailId)
); );
toast.success("Email removed", { toast.success("Email removed", {
description: "Additional email has been removed from your account.", description: "Additional email has been removed from your account.",
@@ -592,7 +641,7 @@ export function Configuration() {
) : ( ) : (
additionalEmails.map((email) => ( additionalEmails.map((email) => (
<div <div
key={email.id} key={email.$id}
className="flex items-center justify-between p-3 border rounded-lg" className="flex items-center justify-between p-3 border rounded-lg"
> >
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@@ -624,7 +673,7 @@ export function Configuration() {
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => removeAdditionalEmail(email.id)} onClick={() => removeAdditionalEmail(email.$id)}
className="text-destructive hover:text-destructive" className="text-destructive hover:text-destructive"
> >
<Trash2 className="h-4 w-4" /> <Trash2 className="h-4 w-4" />

View File

@@ -1,9 +1,11 @@
import { useEffect, useState } from "react"; 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 { Query } from "appwrite";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
import { DebtCard } from "./DebtCard"; import { DebtCard } from "./DebtCard";
import { ConversationTimeline } from "./ConversationTimeline"; // TODO: Migrate these components to Appwrite
import { OnboardingDialog } from "./OnboardingDialog"; // import { ConversationTimeline } from "./ConversationTimeline";
// import { OnboardingDialog } from "./OnboardingDialog";
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "./ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs";
import { Badge } from "./ui/badge"; import { Badge } from "./ui/badge";
@@ -18,6 +20,7 @@ import {
Settings, Settings,
} from "lucide-react"; } from "lucide-react";
import { formatCurrency } from "../lib/utils"; import { formatCurrency } from "../lib/utils";
import type { Models } from "appwrite";
export function Dashboard() { export function Dashboard() {
const [debts, setDebts] = useState<Debt[]>([]); const [debts, setDebts] = useState<Debt[]>([]);
@@ -43,18 +46,18 @@ export function Dashboard() {
const fetchUserProfile = async () => { const fetchUserProfile = 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("*") COLLECTIONS.USER_PROFILES,
.eq("user_id", user.id) [Query.equal('user_id', user.$id)]
.single(); );
setUserProfile(profile); // Get profile for current user
const profile = response.documents[0];
setUserProfile(profile as UserProfile);
// Show onboarding if user hasn't completed it // Show onboarding if user hasn't completed it
if (profile && !profile.onboarding_completed) { if (profile && !profile.onboarding_completed) {
@@ -67,19 +70,17 @@ export function Dashboard() {
const fetchDebts = async () => { const fetchDebts = async () => {
try { try {
const { const user = await account.get();
data: { user },
} = await supabase.auth.getUser();
if (!user) return; if (!user) return;
const { data, error } = await supabase const response = await databases.listDocuments(
.from("debts") DATABASE_ID,
.select("*") COLLECTIONS.DEBTS,
.eq("user_id", user.id) [Query.equal('user_id', user.$id), Query.orderDesc('created_at')]
.order("created_at", { ascending: false }); );
if (error) throw error; // Debts are already filtered and sorted by the query
setDebts(data || []); setDebts(response.documents as Debt[]);
} catch (error) { } catch (error) {
console.error("Error fetching debts:", error); console.error("Error fetching debts:", error);
} finally { } finally {
@@ -88,36 +89,24 @@ export function Dashboard() {
}; };
const setupRealtimeSubscription = () => { const setupRealtimeSubscription = () => {
const subscription = supabase // Appwrite real-time subscription for debts collection
.channel("debts_changes") // Note: This is a simplified version. In production, you'd need to set up proper channels
.on( // and subscribe to document changes for the specific collection
"postgres_changes",
{ // For now, we'll implement a polling mechanism as a fallback
event: "*", // In a full migration, you'd set up Appwrite's real-time listeners
schema: "public", const interval = setInterval(() => {
table: "debts", fetchDebts();
}, }, 30000); // Poll every 30 seconds
(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();
return () => { 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 = () => { const calculateStats = () => {
@@ -149,7 +138,7 @@ export function Dashboard() {
}; };
const handleSignOut = async () => { const handleSignOut = async () => {
await supabase.auth.signOut(); await account.deleteSession('current');
window.location.href = "/"; window.location.href = "/";
}; };
@@ -336,10 +325,11 @@ export function Dashboard() {
</div> </div>
{/* Onboarding Dialog */} {/* Onboarding Dialog */}
<OnboardingDialog {/* TODO: Migrate OnboardingDialog to Appwrite */}
{/* <OnboardingDialog
open={showOnboarding} open={showOnboarding}
onComplete={handleOnboardingComplete} onComplete={handleOnboardingComplete}
/> /> */}
</div> </div>
); );
} }

View File

@@ -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, Query } from "appwrite";
import { toast } from "sonner"; import { toast } from "sonner";
import { formatCurrency } from "../lib/utils"; import { formatCurrency } from "../lib/utils";
import { import {
@@ -53,8 +54,9 @@ import {
getVariablesForTemplate, getVariablesForTemplate,
updateVariablesForTextChange, updateVariablesForTextChange,
} from "../lib/emailVariables"; } from "../lib/emailVariables";
import { ManualResponseDialog } from "./ManualResponseDialog"; // TODO: Migrate these components to Appwrite
import { ConversationTimeline } from "./ConversationTimeline"; // import { ManualResponseDialog } from "./ManualResponseDialog";
// import { ConversationTimeline } from "./ConversationTimeline";
interface DebtCardProps { interface DebtCardProps {
debt: Debt; debt: Debt;
@@ -217,18 +219,12 @@ export function DebtCard({ debt, onUpdate, debts, setDebts }: DebtCardProps) {
}, },
}; };
const { error } = await supabase await databases.updateDocument(
.from("debts") DATABASE_ID,
.update({ metadata: updatedMetadata }) COLLECTIONS.DEBTS,
.eq("id", debt.id); debt.$id,
{ metadata: updatedMetadata }
if (error) { );
console.error("Error saving debt metadata:", error);
toast.error("Error", {
description: "Failed to save email changes. Please try again.",
});
return;
}
// Save variables to database // Save variables to database
await saveVariablesToDatabase(debt.id, variables); await saveVariablesToDatabase(debt.id, variables);
@@ -397,17 +393,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) [Query.equal('user_id', user.$id)]
.single(); );
const profile = response.documents[0];
setUserProfile(profile); setUserProfile(profile);
setHasServerToken(!!profile?.postmark_server_token); setHasServerToken(!!profile?.postmark_server_token);
} catch (error) { } catch (error) {
@@ -427,20 +422,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 +450,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 +484,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 +497,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.",
@@ -618,9 +619,10 @@ export function DebtCard({ debt, onUpdate, debts, setDebts }: DebtCardProps) {
</div> </div>
{/* Manual Response Dialog - show when requires manual review */} {/* Manual Response Dialog - show when requires manual review */}
{debt.status === "requires_manual_review" && ( {/* TODO: Migrate ManualResponseDialog to Appwrite */}
{/* {debt.status === "requires_manual_review" && (
<ManualResponseDialog debt={debt} onResponseSent={onUpdate} /> <ManualResponseDialog debt={debt} onResponseSent={onUpdate} />
)} )} */}
{/* Approve/Reject Buttons */} {/* Approve/Reject Buttons */}
{showApproveRejectButtons() && ( {showApproveRejectButtons() && (
@@ -731,12 +733,13 @@ export function DebtCard({ debt, onUpdate, debts, setDebts }: DebtCardProps) {
</div> </div>
)} )}
<ConversationTimeline {/* TODO: Migrate ConversationTimeline to Appwrite */}
{/* <ConversationTimeline
debt={debt} debt={debt}
onDebtUpdate={(debt) => { onDebtUpdate={(debt) => {
setDebts(debts.map((d) => (d.id === debt.id ? debt : d))); setDebts(debts.map((d) => (d.id === debt.id ? debt : d)));
}} }}
/> /> */}
</CardContent> </CardContent>
</Card> </Card>
); );

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { supabase } from "../lib/supabase"; import { account } from "../lib/appwrite";
import type { User } from "@supabase/supabase-js"; import type { Models } from "appwrite";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
DropdownMenu, DropdownMenu,
@@ -14,25 +14,25 @@ import { BarChart3, LogOut, User as UserIcon, Settings } from "lucide-react";
import { ModeToggle } from "./ModeToggle"; import { ModeToggle } from "./ModeToggle";
export function Navbar() { export function Navbar() {
const [user, setUser] = useState<User | null>(null); const [user, setUser] = useState<Models.User<Models.Preferences> | null>(null);
useEffect(() => { useEffect(() => {
supabase.auth.getSession().then(({ data: { session } }) => { account.get().then((currentUser) => {
setUser(session?.user ?? null); setUser(currentUser);
}).catch(() => {
setUser(null);
}); });
const {
data: { subscription },
} = supabase.auth.onAuthStateChange((event, session) => {
setUser(session?.user ?? null);
});
return () => subscription.unsubscribe();
}, []); }, []);
const handleSignOut = async () => { const handleSignOut = async () => {
await supabase.auth.signOut(); try {
window.location.href = "/"; 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) => { const getInitials = (email: string) => {

173
src/lib/appwrite-admin.ts Normal file
View File

@@ -0,0 +1,173 @@
import { Client, Account, Databases, Functions } from "appwrite";
import { DATABASE_ID, COLLECTIONS } from "./appwrite";
// Export constants for server-side operations
export { DATABASE_ID, COLLECTIONS };
// Create admin client instances for server-side use
const adminClient = createAppwriteAdmin();
export const databases = adminClient.databases;
export const functions = adminClient.functions;
/**
* 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;
}
}

143
src/lib/appwrite.ts Normal file
View File

@@ -0,0 +1,143 @@
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;
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;
id: string;
created_at: string;
debt_id: string;
action: string;
details: Record<string, any>;
};
export type UserProfile = {
$id: string;
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;
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;
id: string;
user_id: string;
month_year: string;
emails_processed: number;
created_at: string;
updated_at: string;
};
export type DebtVariable = {
$id: string;
id: string;
debt_id: string;
variable_name: string;
variable_value: string | null;
created_at: string;
updated_at: string;
};
export type ConversationMessage = {
$id: string;
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;
};

View File

@@ -8,7 +8,8 @@
* - Processing complete email templates * - Processing complete email templates
*/ */
import { supabase } from "./supabase"; import { databases, DATABASE_ID, COLLECTIONS } from "./appwrite-admin";
import { ID } from "appwrite";
export interface VariableProcessingResult { export interface VariableProcessingResult {
processedSubject: string; processedSubject: string;
@@ -68,18 +69,14 @@ export async function loadVariablesFromDatabase(
debtId: string debtId: string
): Promise<Record<string, string>> { ): Promise<Record<string, string>> {
try { try {
const { data: dbVariables, error } = await supabase const response = await databases.listDocuments(
.from("debt_variables") DATABASE_ID,
.select("variable_name, variable_value") COLLECTIONS.DEBT_VARIABLES,
.eq("debt_id", debtId); [`debt_id="${debtId}"`]
);
if (error) {
console.error("Error loading variables from database:", error);
throw error;
}
const loadedVariables: Record<string, string> = {}; const loadedVariables: Record<string, string> = {};
dbVariables?.forEach((dbVar) => { response.documents.forEach((dbVar: any) => {
loadedVariables[dbVar.variable_name] = dbVar.variable_value || ""; loadedVariables[dbVar.variable_name] = dbVar.variable_value || "";
}); });
@@ -100,33 +97,34 @@ export async function saveVariablesToDatabase(
variables: Record<string, string> variables: Record<string, string>
): Promise<void> { ): Promise<void> {
try { try {
// First, delete existing variables for this debt // First, get existing variables for this debt
const { error: deleteError } = await supabase const existingVariables = await databases.listDocuments(
.from("debt_variables") DATABASE_ID,
.delete() COLLECTIONS.DEBT_VARIABLES,
.eq("debt_id", debtId); [`debt_id="${debtId}"`]
);
if (deleteError) { // Delete existing variables
console.error("Error deleting existing variables:", deleteError); for (const variable of existingVariables.documents) {
throw deleteError; await databases.deleteDocument(
DATABASE_ID,
COLLECTIONS.DEBT_VARIABLES,
variable.$id
);
} }
// Then insert new variables // Insert new variables
const variableRecords = Object.entries(variables).map(([name, value]) => ({ for (const [name, value] of Object.entries(variables)) {
debt_id: debtId, await databases.createDocument(
variable_name: name, DATABASE_ID,
variable_value: value, COLLECTIONS.DEBT_VARIABLES,
})); ID.unique(),
{
if (variableRecords.length > 0) { debt_id: debtId,
const { error: insertError } = await supabase variable_name: name,
.from("debt_variables") variable_value: value,
.insert(variableRecords); }
);
if (insertError) {
console.error("Error inserting variables:", insertError);
throw insertError;
}
} }
} catch (error) { } catch (error) {
console.error("Error in saveVariablesToDatabase:", error); console.error("Error in saveVariablesToDatabase:", error);

View File

@@ -1,153 +1,23 @@
import { createClient } from "@supabase/supabase-js"; // This file has been migrated to appwrite-admin.ts
import type { SupabaseClient } from "@supabase/supabase-js"; // The original Supabase admin functionality is now handled by Appwrite
/** /**
* Creates a Supabase client with service role key for server-side operations * @deprecated Use appwrite-admin.ts instead
* This client bypasses Row Level Security (RLS) and should only be used in trusted contexts * This file contained Supabase admin client functionality that has been migrated to Appwrite
* like webhooks, API routes, and server-side functions
*/ */
export function createSupabaseAdmin() { export function createSupabaseAdmin() {
const supabaseUrl = throw new Error('This function has been migrated to Appwrite. Use appwrite-admin.ts instead.');
process.env.PUBLIC_SUPABASE_URL || import.meta.env.PUBLIC_SUPABASE_URL;
const supabaseServiceKey =
process.env.SUPABASE_SERVICE_ROLE_KEY ||
import.meta.env.SUPABASE_SERVICE_ROLE_KEY;
console.log({ supabaseUrl, supabaseServiceKey });
if (!supabaseUrl || !supabaseServiceKey) {
throw new Error(
"Missing Supabase URL or Service Role Key for admin operations"
);
}
return createClient(supabaseUrl, supabaseServiceKey, {
auth: {
autoRefreshToken: false,
persistSession: false,
},
});
} }
/**
* Handle database errors with more user-friendly messages
*/
export function handleDatabaseError(error: any) { export function handleDatabaseError(error: any) {
let errorMessage = error.message; throw new Error('This function has been migrated to Appwrite. Use appwrite-admin.ts instead.');
if (error.message.includes("row-level security")) {
errorMessage = "Database access denied - please check RLS policies";
} else if (error.message.includes("duplicate key")) {
errorMessage = "Duplicate entry detected";
} else if (error.message.includes("foreign key")) {
errorMessage = "Invalid reference in data";
} else if (error.message.includes("not null")) {
errorMessage = "Required field is missing";
}
return {
message: errorMessage,
originalError: process.env.NODE_ENV === "development" ? error : undefined,
};
} }
/** export async function getUserIdByEmail(email: string): Promise<string | null> {
* Find user ID by email address (primary or additional email) throw new Error('This function has been migrated to Appwrite. Use appwrite-admin.ts instead.');
* First checks public.users table, then additional_emails if needed
*/
export async function getUserIdByEmail(
email: string,
supabaseAdmin?: SupabaseClient
): Promise<string | null> {
const client = supabaseAdmin || createSupabaseAdmin();
try {
// First try to find user by primary email in public.users table
const { data: primaryUser, error: primaryError } = await client
.from("users")
.select("id")
.eq("email", email.toLowerCase())
.maybeSingle();
if (primaryError) {
console.error("Error finding user by primary email:", primaryError);
}
if (primaryUser) {
return primaryUser.id;
}
// If not found, check additional emails
const { data: additionalEmail, error: additionalError } = await client
.from("additional_emails")
.select("user_id")
.eq("email_address", email.toLowerCase())
// TODO: START REQUIRING VERIFIED ADDITIONAL EMAILS
// .eq("verified", true)
.eq("verified", false)
.maybeSingle();
if (additionalError) {
console.error("Error finding user by additional email:", additionalError);
return null;
}
return additionalEmail?.user_id || null;
} catch (error) {
console.error("Error in getUserIdByEmail:", error);
return null;
}
} }
/** export async function getUserByEmail(email: string) {
* Get full user information by email address (primary or additional email) throw new Error('This function has been migrated to Appwrite. Use appwrite-admin.ts instead.');
* First checks public.users table, then additional_emails if needed
*/
export async function getUserByEmail(
email: string,
supabaseAdmin?: SupabaseClient
) {
const client = supabaseAdmin || createSupabaseAdmin();
try {
// First try to find user by primary email in public.users table
const { data: primaryUser, error: primaryError } = await client
.from("users")
.select("*")
.eq("email", email.toLowerCase())
.maybeSingle();
if (primaryError) {
console.error("Error finding user by primary email:", primaryError);
}
if (primaryUser) {
return primaryUser;
}
// If not found, check additional emails and join with users table
const { data: userViaAdditionalEmail, error: additionalError } = await client
.from("additional_emails")
.select(`
user_id,
users!inner (
id,
email,
created_at
)
`)
.eq("email_address", email.toLowerCase())
.eq("verified", true)
.maybeSingle();
if (additionalError) {
console.error("Error finding user by additional email:", additionalError);
return null;
}
return userViaAdditionalEmail?.users || null;
} catch (error) {
console.error("Error in getUserByEmail:", error);
return null;
}
} }

View File

@@ -1,13 +1,5 @@
import { createClient } from "@supabase/supabase-js"; // Type definitions for Appwrite migration
// This file no longer contains Supabase client - use appwrite.ts instead
const supabaseUrl = import.meta.env.PUBLIC_SUPABASE_URL;
const supabaseAnonKey = import.meta.env.PUBLIC_SUPABASE_ANON_KEY;
if (!supabaseUrl || !supabaseAnonKey) {
throw new Error("Missing Supabase environment variables");
}
export const supabase = createClient(supabaseUrl, supabaseAnonKey);
export type User = { export type User = {
id: string; id: string;

View File

@@ -1,13 +1,14 @@
import type { APIRoute } from "astro"; import type { APIRoute } from "astro";
import { import {
createSupabaseAdmin, createAppwriteAdmin,
getUserIdByEmail, getUserIdByEmail,
handleDatabaseError, handleDatabaseError,
} from "../../lib/supabase-admin"; } from "../../lib/appwrite-admin";
import { generateObject } from "ai"; import { generateObject } from "ai";
import { createGoogleGenerativeAI } from "@ai-sdk/google"; import { createGoogleGenerativeAI } from "@ai-sdk/google";
import { z } from "zod"; import { z } from "zod";
import type { SupabaseClient } from "@supabase/supabase-js"; import { DATABASE_ID, COLLECTIONS } from "../../lib/appwrite";
import { ID, Query } from "appwrite";
// Schema for debt information extraction // Schema for debt information extraction
const debtSchema = z.object({ const debtSchema = z.object({
@@ -125,19 +126,52 @@ async function parseDebtWithAI(emailText: string, fromEmail: string) {
// Function to increment email processing usage // Function to increment email processing usage
async function incrementEmailUsage( async function incrementEmailUsage(
userId: string, userId: string,
supabaseAdmin: SupabaseClient, appwriteAdmin: ReturnType<typeof createAppwriteAdmin>,
) { ) {
try { try {
// Call the database function to increment usage // In Appwrite, we'll need to implement this differently since there are no stored procedures
const { error } = await supabaseAdmin.rpc("increment_email_usage", { // For now, we'll implement a simple increment by finding the current month's usage and updating it
target_user_id: userId,
});
if (error) { const currentDate = new Date();
console.error("Error incrementing email usage:", error); 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,
[Query.equal('user_id', userId), Query.equal('month_year', monthYear)]
);
const existingUsage = response.documents[0];
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) { } catch (error) {
console.error("Error calling increment_email_usage:", error); console.error("Error incrementing email usage:", error);
} }
} }
@@ -145,25 +179,25 @@ async function incrementEmailUsage(
async function checkForExistingNegotiation( async function checkForExistingNegotiation(
fromEmail: string, fromEmail: string,
toEmail: string, toEmail: string,
supabaseAdmin: any, appwriteAdmin: ReturnType<typeof createAppwriteAdmin>,
) { ) {
try { try {
// Look for debts where we've sent emails to this fromEmail and are awaiting response // 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 response = await appwriteAdmin.databases.listDocuments(
const { data: debts, error } = await supabaseAdmin DATABASE_ID,
.from("debts") COLLECTIONS.DEBTS,
.select("*") [Query.in('status', ['sent', 'awaiting_response', 'counter_negotiating']), Query.orderDesc('last_message_at')]
.in("status", ["sent", "awaiting_response", "counter_negotiating"]) );
.contains("metadata", { fromEmail: fromEmail, toEmail: toEmail })
.order("last_message_at", { ascending: false });
if (error) { // Find matching debts based on email metadata
console.error("Error checking for existing negotiation:", error); const matchingDebts = response.documents.filter(debt => {
return null; const metadata = debt.metadata as any;
} return metadata?.fromEmail === fromEmail &&
metadata?.toEmail === toEmail;
});
// Return the most recent debt that matches // Return the most recent debt that matches (already sorted by orderDesc in query)
return debts && debts.length > 0 ? debts[0] : null; return matchingDebts.length > 0 ? matchingDebts[0] : null;
} catch (error) { } catch (error) {
console.error("Error in checkForExistingNegotiation:", error); console.error("Error in checkForExistingNegotiation:", error);
return null; return null;
@@ -174,7 +208,7 @@ async function checkForExistingNegotiation(
async function handleNegotiationResponse( async function handleNegotiationResponse(
debt: any, debt: any,
emailData: any, emailData: any,
supabaseAdmin: any, appwriteAdmin: ReturnType<typeof createAppwriteAdmin>,
) { ) {
try { try {
const textBody = emailData.TextBody || emailData.HtmlBody || ""; const textBody = emailData.TextBody || emailData.HtmlBody || "";
@@ -183,45 +217,58 @@ async function handleNegotiationResponse(
const messageId = emailData.MessageID || `inbound-${Date.now()}`; const messageId = emailData.MessageID || `inbound-${Date.now()}`;
// First, record this message in the conversation // First, record this message in the conversation
await supabaseAdmin.from("conversation_messages").insert({ await appwriteAdmin.databases.createDocument(
debt_id: debt.id, DATABASE_ID,
message_type: "response_received", COLLECTIONS.CONVERSATION_MESSAGES,
direction: "inbound", ID.unique(),
subject: subject, {
body: textBody, debt_id: debt.$id,
from_email: fromEmail, message_type: "response_received",
to_email: emailData.ToFull?.[0]?.Email || emailData.To || "", direction: "inbound",
message_id: messageId, 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 // Update debt conversation tracking
await supabaseAdmin await appwriteAdmin.databases.updateDocument(
.from("debts") DATABASE_ID,
.update({ COLLECTIONS.DEBTS,
debt.$id,
{
conversation_count: debt.conversation_count + 1, conversation_count: debt.conversation_count + 1,
last_message_at: new Date().toISOString(), last_message_at: new Date().toISOString(),
status: "counter_negotiating", // Temporary status while analyzing status: "counter_negotiating", // Temporary status while analyzing
}) updated_at: new Date().toISOString()
.eq("id", debt.id); }
);
// Call the analyze-response function // Call the analyze-response function
const supabaseUrl = process.env.SUPABASE_URL || const appwriteEndpoint = process.env.PUBLIC_APPWRITE_ENDPOINT ||
import.meta.env.PUBLIC_SUPABASE_URL; import.meta.env.PUBLIC_APPWRITE_ENDPOINT;
const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY || const appwriteProjectId = process.env.PUBLIC_APPWRITE_PROJECT_ID ||
import.meta.env.SUPABASE_SERVICE_ROLE_KEY; import.meta.env.PUBLIC_APPWRITE_PROJECT_ID;
const appwriteApiKey = process.env.APPWRITE_API_KEY ||
import.meta.env.APPWRITE_API_KEY;
if (supabaseUrl && supabaseServiceKey) { if (appwriteEndpoint && appwriteProjectId && appwriteApiKey) {
const analyzeUrl = `${supabaseUrl}/functions/v1/analyze-response`; const analyzeUrl = `${appwriteEndpoint}/functions/v1/analyze-response`;
try { try {
const response = await fetch(analyzeUrl, { const response = await fetch(analyzeUrl, {
method: "POST", method: "POST",
headers: { headers: {
Authorization: `Bearer ${supabaseServiceKey}`, "X-Appwrite-Project": appwriteProjectId,
"X-Appwrite-Key": appwriteApiKey,
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify({
debtId: debt.id, debtId: debt.$id,
fromEmail, fromEmail,
subject, subject,
body: textBody, body: textBody,
@@ -233,20 +280,6 @@ async function handleNegotiationResponse(
const result = await response.json(); const result = await response.json();
console.log("Response analysis completed:", result); 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( return new Response(
JSON.stringify({ JSON.stringify({
success: true, success: true,
@@ -270,22 +303,22 @@ async function handleNegotiationResponse(
} }
// Fallback: just log the response and mark for manual review // Fallback: just log the response and mark for manual review
await supabaseAdmin.from("audit_logs").insert({ await appwriteAdmin.databases.createDocument(
debt_id: debt.id, DATABASE_ID,
action: "response_received_fallback", COLLECTIONS.AUDIT_LOGS,
details: { ID.unique(),
fromEmail, {
subject, debt_id: debt.$id,
bodyPreview: textBody.substring(0, 200), action: "response_received_fallback",
requiresManualReview: true, details: {
}, fromEmail,
}); subject,
bodyPreview: textBody.substring(0, 200),
// Update status to require user review requiresManualReview: true,
// await supabaseAdmin },
// .from("debts") created_at: new Date().toISOString()
// .update({ status: "awaiting_response" }) }
// .eq("id", debt.id); );
return new Response( return new Response(
JSON.stringify({ success: true, message: "Response logged" }), JSON.stringify({ success: true, message: "Response logged" }),
@@ -308,12 +341,12 @@ async function handleNegotiationResponse(
export const POST: APIRoute = async ({ request }) => { export const POST: APIRoute = async ({ request }) => {
try { try {
// Create service role client for webhook operations (bypasses RLS) // Create admin client for webhook operations
let supabaseAdmin; let appwriteAdmin;
try { try {
supabaseAdmin = createSupabaseAdmin(); appwriteAdmin = createAppwriteAdmin();
} catch (configError) { } catch (configError) {
console.error("Supabase admin configuration error:", configError); console.error("Appwrite admin configuration error:", configError);
return new Response( return new Response(
JSON.stringify({ error: "Server configuration error" }), JSON.stringify({ error: "Server configuration error" }),
{ {
@@ -339,7 +372,7 @@ export const POST: APIRoute = async ({ request }) => {
const toEmail = data.ToFull?.[0]?.Email || data.To || ""; const toEmail = data.ToFull?.[0]?.Email || data.To || "";
// Find the user who should receive this debt // Find the user who should receive this debt
const userId = await getUserIdByEmail(toEmail, supabaseAdmin); const userId = await getUserIdByEmail(toEmail, appwriteAdmin);
if (!userId) { if (!userId) {
console.warn(`No user found for email: ${toEmail}`); console.warn(`No user found for email: ${toEmail}`);
return new Response("No matching user found", { status: 200 }); return new Response("No matching user found", { status: 200 });
@@ -349,19 +382,19 @@ export const POST: APIRoute = async ({ request }) => {
const existingDebt = await checkForExistingNegotiation( const existingDebt = await checkForExistingNegotiation(
fromEmail, fromEmail,
toEmail, toEmail,
supabaseAdmin, appwriteAdmin,
); );
console.log({ existingDebt, fromEmail, toEmail }); console.log({ existingDebt, fromEmail, toEmail });
if (existingDebt) { if (existingDebt) {
console.log( 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 // Increment email processing usage
await incrementEmailUsage(userId, supabaseAdmin); await incrementEmailUsage(userId, appwriteAdmin);
// Check for opt-out using AI // Check for opt-out using AI
const optOutDetection = await detectOptOutWithAI(textBody, fromEmail); const optOutDetection = await detectOptOutWithAI(textBody, fromEmail);
@@ -383,15 +416,22 @@ export const POST: APIRoute = async ({ request }) => {
if (hasOptOut) { if (hasOptOut) {
// Log opt-out and don't process further // Log opt-out and don't process further
const { error } = await supabaseAdmin.from("debts").insert({ try {
user_id: userId, await appwriteAdmin.databases.createDocument(
vendor: fromEmail, DATABASE_ID,
amount: 0, COLLECTIONS.DEBTS,
raw_email: textBody, ID.unique(),
status: "opted_out", {
}); user_id: userId,
vendor: fromEmail,
if (error) { 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); console.error("Error logging opt-out:", error);
const errorInfo = handleDatabaseError(error); const errorInfo = handleDatabaseError(error);
return new Response(JSON.stringify({ error: errorInfo.message }), { return new Response(JSON.stringify({ error: errorInfo.message }), {
@@ -418,44 +458,54 @@ export const POST: APIRoute = async ({ request }) => {
} }
// Insert debt record with AI-extracted information // Insert debt record with AI-extracted information
const { data: insertedDebt, error: insertError } = await supabaseAdmin let insertedDebt;
.from("debts") try {
.insert({ insertedDebt = await appwriteAdmin.databases.createDocument(
user_id: userId, DATABASE_ID,
vendor: debtInfo.vendor, COLLECTIONS.DEBTS,
amount: debtInfo.amount, ID.unique(),
raw_email: textBody, {
status: "received", user_id: userId,
description: debtInfo.description, vendor: debtInfo.vendor,
due_date: debtInfo.dueDate, amount: debtInfo.amount,
conversation_count: 1, raw_email: textBody,
last_message_at: new Date().toISOString(), status: "received",
negotiation_round: 1, description: debtInfo.description,
metadata: { due_date: debtInfo.dueDate,
isDebtCollection: debtInfo.isDebtCollection, conversation_count: 1,
subject: data.Subject, last_message_at: new Date().toISOString(),
fromEmail: fromEmail, negotiation_round: 1,
toEmail: toEmail, projected_savings: 0,
}, metadata: {
}) isDebtCollection: debtInfo.isDebtCollection,
.select() subject: data.Subject,
.single(); 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 // Record the initial debt email as the first conversation message
await supabaseAdmin.from("conversation_messages").insert({ await appwriteAdmin.databases.createDocument(
debt_id: insertedDebt.id, DATABASE_ID,
message_type: "initial_debt", COLLECTIONS.CONVERSATION_MESSAGES,
direction: "inbound", ID.unique(),
subject: data.Subject, {
body: textBody, debt_id: insertedDebt.$id,
from_email: fromEmail, message_type: "initial_debt",
to_email: toEmail, direction: "inbound",
message_id: data.MessageID || `initial-${Date.now()}`, subject: data.Subject,
}); body: textBody,
} from_email: fromEmail,
to_email: toEmail,
if (insertError) { 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); console.error("Error inserting debt:", insertError);
const errorInfo = handleDatabaseError(insertError); const errorInfo = handleDatabaseError(insertError);
@@ -472,33 +522,42 @@ export const POST: APIRoute = async ({ request }) => {
} }
// Log the email receipt // Log the email receipt
await supabaseAdmin.from("audit_logs").insert({ await appwriteAdmin.databases.createDocument(
debt_id: insertedDebt.id, DATABASE_ID,
action: "email_received", COLLECTIONS.AUDIT_LOGS,
details: { ID.unique(),
vendor: debtInfo.vendor, {
amount: debtInfo.amount, debt_id: insertedDebt.$id,
subject: data.Subject, action: "email_received",
aiParsed: true, 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 // Trigger negotiation function if this is a legitimate debt
if (debtInfo.amount > 0 && debtInfo.isDebtCollection) { if (debtInfo.amount > 0 && debtInfo.isDebtCollection) {
// Access environment variables through Astro runtime // Access environment variables through Astro runtime
const supabaseUrl = process.env.SUPABASE_URL || const appwriteEndpoint = process.env.PUBLIC_APPWRITE_ENDPOINT ||
import.meta.env.PUBLIC_SUPABASE_URL; import.meta.env.PUBLIC_APPWRITE_ENDPOINT;
const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY || const appwriteProjectId = process.env.PUBLIC_APPWRITE_PROJECT_ID ||
import.meta.env.SUPABASE_SERVICE_ROLE_KEY; import.meta.env.PUBLIC_APPWRITE_PROJECT_ID;
const appwriteApiKey = process.env.APPWRITE_API_KEY ||
import.meta.env.APPWRITE_API_KEY;
if (supabaseUrl && supabaseServiceKey) { if (appwriteEndpoint && appwriteProjectId && appwriteApiKey) {
const negotiateUrl = `${supabaseUrl}/functions/v1/negotiate`; const negotiateUrl = `${appwriteEndpoint}/functions/v1/negotiate`;
try { try {
await fetch(negotiateUrl, { await fetch(negotiateUrl, {
method: "POST", method: "POST",
headers: { headers: {
Authorization: `Bearer ${supabaseServiceKey}`, "X-Appwrite-Project": appwriteProjectId,
"X-Appwrite-Key": appwriteApiKey,
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ record: insertedDebt }), body: JSON.stringify({ record: insertedDebt }),
@@ -509,7 +568,7 @@ export const POST: APIRoute = async ({ request }) => {
} }
} else { } else {
console.warn( console.warn(
"Supabase environment variables not configured for negotiation trigger", "Appwrite environment variables not configured for negotiation trigger",
); );
} }
} }