mirror of
https://github.com/FranP-code/inbox-negotiator.git
synced 2025-10-13 00:42:26 +00:00
855 lines
29 KiB
TypeScript
855 lines
29 KiB
TypeScript
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
|
|
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
|
|
import { generateObject } from "https://esm.sh/ai@3.4.7";
|
|
import { createGoogleGenerativeAI } from "https://esm.sh/@ai-sdk/google@0.0.52";
|
|
import { z } from "https://esm.sh/zod@3.22.4";
|
|
|
|
const corsHeaders = {
|
|
"Access-Control-Allow-Origin": "*",
|
|
"Access-Control-Allow-Headers":
|
|
"authorization, x-client-info, apikey, content-type",
|
|
};
|
|
|
|
// Schema for AI response analysis
|
|
const responseAnalysisSchema = z.object({
|
|
intent: z.enum([
|
|
"acceptance",
|
|
"rejection",
|
|
"counter_offer",
|
|
"request_info",
|
|
"unclear",
|
|
])
|
|
.describe("The primary intent of the response"),
|
|
sentiment: z.enum(["positive", "negative", "neutral"])
|
|
.describe("Overall sentiment of the response"),
|
|
confidence: z.number().min(0).max(1)
|
|
.describe("Confidence in the intent classification"),
|
|
extractedTerms: z.object({
|
|
proposedAmount: z.number().optional().describe(
|
|
"Any amount mentioned in response",
|
|
),
|
|
proposedPaymentPlan: z.string().optional().describe(
|
|
"Payment plan details if mentioned",
|
|
),
|
|
paymentTerms: z.object({
|
|
monthlyAmount: z.number().optional().describe("Monthly payment amount"),
|
|
numberOfPayments: z.number().optional().describe(
|
|
"Number of payments/installments",
|
|
),
|
|
totalAmount: z.number().optional().describe("Total amount to be paid"),
|
|
interestRate: z.number().optional().describe(
|
|
"Interest rate if applicable",
|
|
),
|
|
paymentFrequency: z.string().optional().describe(
|
|
"Payment frequency (monthly, weekly, etc.)",
|
|
),
|
|
}).optional().describe("Structured payment plan terms"),
|
|
deadline: z.string().optional().describe("Any deadline mentioned"),
|
|
conditions: z.array(z.string()).optional().describe(
|
|
"Any conditions or requirements mentioned",
|
|
),
|
|
}).describe("Key terms extracted from the response"),
|
|
reasoning: z.string().describe("Explanation of the analysis"),
|
|
suggestedNextAction: z.enum([
|
|
"accept_offer",
|
|
"send_counter",
|
|
"request_clarification",
|
|
"escalate_to_user",
|
|
"mark_settled",
|
|
]).describe("Recommended next action"),
|
|
requiresUserReview: z.boolean().describe(
|
|
"Whether this response needs human review",
|
|
),
|
|
});
|
|
|
|
interface EmailResponseData {
|
|
debtId: string;
|
|
fromEmail: string;
|
|
subject: string;
|
|
body: string;
|
|
messageId?: string;
|
|
}
|
|
|
|
// Retrieve full conversation history for context
|
|
async function getConversationHistory(
|
|
supabaseClient: any,
|
|
debtId: string,
|
|
) {
|
|
try {
|
|
const { data: messages, error } = await supabaseClient
|
|
.from("conversation_messages")
|
|
.select("*")
|
|
.eq("debt_id", debtId)
|
|
.order("created_at", { ascending: true });
|
|
|
|
if (error) {
|
|
console.error("Error fetching conversation history:", error);
|
|
return [];
|
|
}
|
|
|
|
return messages || [];
|
|
} catch (error) {
|
|
console.error("Error in getConversationHistory:", error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// AI-powered response analysis
|
|
async function analyzeEmailResponse(
|
|
supabaseClient: any,
|
|
debtId: string,
|
|
fromEmail: string,
|
|
subject: string,
|
|
body: string,
|
|
originalNegotiation?: any,
|
|
) {
|
|
try {
|
|
const googleApiKey = Deno.env.get("GOOGLE_GENERATIVE_AI_API_KEY");
|
|
if (!googleApiKey) {
|
|
console.warn("Google API key not configured, using fallback analysis");
|
|
return getFallbackAnalysis(body);
|
|
}
|
|
|
|
console.log("Analyzing email with AI:", {
|
|
fromEmail,
|
|
subject,
|
|
bodyLength: body.length,
|
|
});
|
|
|
|
// Get full conversation history for better context
|
|
const conversationHistory = await getConversationHistory(
|
|
supabaseClient,
|
|
debtId,
|
|
);
|
|
|
|
console.log({
|
|
debtId,
|
|
fromEmail,
|
|
subject,
|
|
body,
|
|
originalNegotiation,
|
|
conversationHistoryLength: conversationHistory.length,
|
|
});
|
|
|
|
const system =
|
|
`You are an expert financial analysis AI. Your sole function is to meticulously analyze creditor emails and populate a structured JSON object that conforms to the provided schema.
|
|
|
|
Your entire output MUST be a single, valid JSON object. Do not include any markdown, explanations, or conversational text outside of the JSON structure itself.
|
|
|
|
--- FIELD-BY-FIELD INSTRUCTIONS ---
|
|
|
|
1. **intent**: Classify the creditor's primary intent.
|
|
- "acceptance": Clear agreement to our original proposal.
|
|
- "rejection": Clear refusal of our proposal without offering new terms.
|
|
- "counter_offer": Proposes ANY new financial terms (different amount, payment plan, etc.). This is the most common intent besides acceptance.
|
|
- "request_info": Asks for more information (e.g., "Can you provide proof of hardship?").
|
|
- "unclear": The purpose of the email cannot be determined.
|
|
|
|
2. **sentiment**: Classify the emotional tone of the email.
|
|
- "positive": Cooperative, polite, agreeable language.
|
|
- "negative": Hostile, demanding, or threatening language.
|
|
- "neutral": Strictly professional, factual, and devoid of emotional language.
|
|
|
|
3. **confidence**: Provide a score from 0.0 to 1.0 for your "intent" classification. (e.g., 0.95).
|
|
|
|
4. **extractedTerms**:
|
|
- **proposedAmount**: Extract a single, lump-sum settlement amount if offered. If the offer is a payment plan, this field should be null.
|
|
- **proposedPaymentPlan**: Capture the payment plan offer as a descriptive string, exactly as the creditor states it. e.g., "$100 a month for 12 months". If no plan is mentioned, this is null.
|
|
- **paymentTerms**: If a payment plan is mentioned, break it down into its structured components here. If no plan is mentioned, this entire object should be null.
|
|
- **monthlyAmount**: The specific amount for each payment.
|
|
- **numberOfPayments**: The number of installments.
|
|
- **totalAmount**: The total payout of the plan, ONLY if explicitly stated (e.g., "...totaling $1200").
|
|
- **interestRate**: The interest rate as a number (e.g., for "5% interest", extract 5).
|
|
- **paymentFrequency**: The frequency (e.g., "monthly", "weekly", "bi-weekly").
|
|
- **deadline**: Extract any specific date or timeframe for action (e.g., "by June 30th", "within 10 days").
|
|
- **conditions**: Extract all non-financial requirements as an array of strings. Example: ["Payment must be via certified funds", "A settlement agreement must be signed"]. If none, use an empty array [].
|
|
|
|
5. **reasoning**: Briefly explain the logic behind your "intent" and "suggestedNextAction" classifications, referencing parts of the email.
|
|
|
|
6. **suggestedNextAction**: Recommend the most logical business action.
|
|
- "accept_offer": The creditor's offer matches or is better than our goal.
|
|
- "send_counter": The creditor made a counter-offer that we should negotiate.
|
|
- "request_clarification": The email is ambiguous or missing key information.
|
|
- "escalate_to_user": The response is hostile, contains legal threats, is complex, or requires a human decision.
|
|
- "mark_settled": The email confirms the debt is fully settled and no further action is needed.
|
|
|
|
7. **requiresUserReview**: Set to 'true' if intent is "unclear", sentiment is "negative", confidence is below 0.85, the email contains unusual legal language, the "suggestedNextAction" is "escalate_to_user", OR if the message is vague/lacks specific financial terms (e.g., "think of a better offer", "we need more", etc.). Only set to 'false' for clear, specific counter-offers with concrete terms.`;
|
|
|
|
// Build conversation history context
|
|
// const conversationContext = conversationHistory.length > 0
|
|
// ? `--- FULL CONVERSATION HISTORY ---
|
|
// ${
|
|
// conversationHistory.map((msg, index) =>
|
|
// `${
|
|
// index + 1
|
|
// }. ${msg.direction.toUpperCase()} - ${msg.message_type} (${
|
|
// new Date(msg.created_at).toLocaleDateString()
|
|
// })
|
|
// Subject: ${msg.subject || "N/A"}
|
|
// Body: ${msg.body.substring(0, 500)}${msg.body.length > 500 ? "..." : ""}
|
|
// ${msg.ai_analysis ? `AI Analysis: ${JSON.stringify(msg.ai_analysis)}` : ""}
|
|
// ---`
|
|
// ).join("\n")
|
|
// }
|
|
|
|
// `
|
|
// : "";
|
|
|
|
const conversationContext = conversationHistory.length > 0
|
|
? `--- FULL CONVERSATION HISTORY ---
|
|
${
|
|
conversationHistory.map((msg, index) =>
|
|
`${
|
|
index + 1
|
|
}. ${msg.direction.toUpperCase()} - ${msg.message_type} (${
|
|
new Date(msg.created_at).toLocaleDateString()
|
|
})
|
|
Subject: ${msg.subject || "N/A"}
|
|
Body: ${msg.body}
|
|
${msg.ai_analysis ? `AI Analysis: ${JSON.stringify(msg.ai_analysis)}` : ""}
|
|
---`
|
|
).join("\n")
|
|
}
|
|
|
|
`
|
|
: "";
|
|
|
|
const prompt =
|
|
`Analyze the following email and extract the financial details and intent, populating the JSON object according to your system instructions.
|
|
|
|
--- CURRENT EMAIL TO ANALYZE ---
|
|
From: ${fromEmail}
|
|
Subject: ${subject}
|
|
Body: ${body}
|
|
|
|
${conversationContext}
|
|
|
|
${
|
|
originalNegotiation
|
|
? `--- MOST RECENT NEGOTIATION CONTEXT ---
|
|
Our Negotiation Strategy: ${originalNegotiation.strategy}
|
|
Our Proposed Amount: $${originalNegotiation.proposedAmount || "N/A"}
|
|
Our Proposed Terms: ${originalNegotiation.terms || "N/A"}
|
|
Our Reasoning: ${originalNegotiation.reasoning || "N/A"}
|
|
Our Latest Email Body: ${originalNegotiation.body || "N/A"}
|
|
`
|
|
: ""
|
|
}`;
|
|
|
|
console.log("AI Analysis System:", system);
|
|
console.log("AI Analysis Prompt:", prompt);
|
|
|
|
const result = await generateObject({
|
|
model: createGoogleGenerativeAI({
|
|
apiKey: googleApiKey,
|
|
})("gemini-2.5-flash-preview-04-17"),
|
|
system,
|
|
prompt,
|
|
schema: responseAnalysisSchema,
|
|
});
|
|
|
|
console.log("AI Analysis Result:", JSON.stringify(result.object, null, 2));
|
|
console.log(
|
|
"Extracted Terms:",
|
|
JSON.stringify(result.object.extractedTerms, null, 2),
|
|
);
|
|
|
|
return result.object;
|
|
} catch (error) {
|
|
console.error("AI response analysis error:", error);
|
|
console.log("Falling back to regex-based analysis");
|
|
return getFallbackAnalysis(body);
|
|
}
|
|
}
|
|
|
|
// Calculate financial outcome when offer is accepted
|
|
function calculateFinancialOutcome(debt: any, analysis: any): any {
|
|
try {
|
|
const originalAmount = debt.amount || 0;
|
|
let acceptedAmount = originalAmount;
|
|
let paymentStructure = null;
|
|
let financialBenefit = null;
|
|
|
|
// Try to extract accepted amount from AI analysis
|
|
if (analysis.extractedTerms?.proposedAmount) {
|
|
acceptedAmount = analysis.extractedTerms.proposedAmount;
|
|
} else if (analysis.extractedTerms?.paymentTerms?.totalAmount) {
|
|
acceptedAmount = analysis.extractedTerms.paymentTerms.totalAmount;
|
|
} else if (debt.metadata?.prospectedSavings?.amount) {
|
|
// Fall back to original negotiation terms if no specific amount mentioned
|
|
acceptedAmount = originalAmount - debt.metadata.prospectedSavings.amount;
|
|
}
|
|
|
|
// Analyze payment structure if present
|
|
if (analysis.extractedTerms?.paymentTerms) {
|
|
const terms = analysis.extractedTerms.paymentTerms;
|
|
paymentStructure = {
|
|
type: "installment_plan",
|
|
monthlyAmount: terms.monthlyAmount,
|
|
numberOfPayments: terms.numberOfPayments,
|
|
totalAmount: terms.totalAmount || acceptedAmount,
|
|
frequency: terms.paymentFrequency || "monthly",
|
|
interestRate: terms.interestRate || 0,
|
|
};
|
|
|
|
// Calculate time value and cash flow benefits
|
|
if (terms.monthlyAmount && terms.numberOfPayments) {
|
|
const totalPayments = terms.monthlyAmount * terms.numberOfPayments;
|
|
const timeToComplete = terms.numberOfPayments; // in months
|
|
|
|
financialBenefit = {
|
|
type: "payment_restructuring",
|
|
principalReduction: Math.max(0, originalAmount - totalPayments),
|
|
cashFlowRelief: {
|
|
monthlyReduction: originalAmount - terms.monthlyAmount,
|
|
extendedTermMonths: timeToComplete,
|
|
totalCashFlowBenefit: (originalAmount - terms.monthlyAmount) *
|
|
timeToComplete,
|
|
},
|
|
timeValueBenefit: calculateTimeValueBenefit(
|
|
originalAmount,
|
|
terms.monthlyAmount,
|
|
timeToComplete,
|
|
),
|
|
};
|
|
}
|
|
}
|
|
|
|
// Calculate actual savings (principal reduction)
|
|
const actualSavings = Math.max(0, originalAmount - acceptedAmount);
|
|
|
|
// Determine the primary financial benefit
|
|
if (actualSavings > 0) {
|
|
financialBenefit = {
|
|
type: "principal_reduction",
|
|
amount: actualSavings,
|
|
percentage: (actualSavings / originalAmount * 100).toFixed(2),
|
|
description: `${
|
|
((actualSavings / originalAmount) * 100).toFixed(1)
|
|
}% principal reduction`,
|
|
};
|
|
} else if (paymentStructure && paymentStructure.monthlyAmount) {
|
|
// No principal reduction but payment restructuring
|
|
const monthlyReduction = originalAmount - paymentStructure.monthlyAmount;
|
|
financialBenefit = {
|
|
type: "payment_restructuring",
|
|
monthlyReduction: monthlyReduction,
|
|
extendedTermMonths: paymentStructure.numberOfPayments,
|
|
description:
|
|
`Payment restructured to $${paymentStructure.monthlyAmount}/month over ${paymentStructure.numberOfPayments} months`,
|
|
cashFlowBenefit: monthlyReduction > 0
|
|
? `$${monthlyReduction}/month cash flow relief`
|
|
: "Extended payment terms",
|
|
};
|
|
}
|
|
|
|
console.log(
|
|
`Financial outcome: Original: $${originalAmount}, Accepted: $${acceptedAmount}, Savings: $${actualSavings}`,
|
|
);
|
|
|
|
return {
|
|
actualSavings,
|
|
acceptedAmount,
|
|
paymentStructure,
|
|
financialBenefit,
|
|
originalAmount,
|
|
};
|
|
} catch (error) {
|
|
console.error("Error calculating financial outcome:", error);
|
|
// Return basic fallback
|
|
return {
|
|
actualSavings: debt.projected_savings || 0,
|
|
acceptedAmount: debt.amount,
|
|
paymentStructure: null,
|
|
financialBenefit: null,
|
|
originalAmount: debt.amount,
|
|
};
|
|
}
|
|
}
|
|
|
|
// Calculate time value benefit of extended payment terms
|
|
function calculateTimeValueBenefit(
|
|
originalAmount: number,
|
|
monthlyPayment: number,
|
|
months: number,
|
|
): any {
|
|
// Simple present value calculation assuming 5% annual discount rate
|
|
const monthlyRate = 0.05 / 12;
|
|
let presentValue = 0;
|
|
|
|
for (let i = 1; i <= months; i++) {
|
|
presentValue += monthlyPayment / Math.pow(1 + monthlyRate, i);
|
|
}
|
|
|
|
const timeValueBenefit = originalAmount - presentValue;
|
|
|
|
return {
|
|
presentValueOfPayments: presentValue.toFixed(2),
|
|
timeValueBenefit: timeValueBenefit.toFixed(2),
|
|
effectiveDiscount: ((timeValueBenefit / originalAmount) * 100).toFixed(2),
|
|
};
|
|
}
|
|
|
|
// Fallback analysis when AI is unavailable
|
|
function getFallbackAnalysis(
|
|
body: string,
|
|
): typeof responseAnalysisSchema._type {
|
|
const lowerBody = body.toLowerCase();
|
|
|
|
// Simple keyword-based analysis
|
|
let intent:
|
|
| "acceptance"
|
|
| "rejection"
|
|
| "counter_offer"
|
|
| "request_info"
|
|
| "unclear" = "unclear";
|
|
let sentiment: "positive" | "negative" | "neutral" = "neutral";
|
|
|
|
if (
|
|
lowerBody.includes("accept") || lowerBody.includes("agree") ||
|
|
lowerBody.includes("approved")
|
|
) {
|
|
intent = "acceptance";
|
|
sentiment = "positive";
|
|
} else if (
|
|
lowerBody.includes("reject") || lowerBody.includes("decline") ||
|
|
lowerBody.includes("denied")
|
|
) {
|
|
intent = "rejection";
|
|
sentiment = "negative";
|
|
} else if (
|
|
lowerBody.includes("counter") || lowerBody.includes("instead") ||
|
|
lowerBody.includes("however")
|
|
) {
|
|
intent = "counter_offer";
|
|
sentiment = "neutral";
|
|
} else if (
|
|
lowerBody.includes("information") || lowerBody.includes("clarify") ||
|
|
lowerBody.includes("details")
|
|
) {
|
|
intent = "request_info";
|
|
sentiment = "neutral";
|
|
}
|
|
|
|
// Enhanced extraction using multiple regex patterns
|
|
const extractFinancialTerms = (text: string) => {
|
|
// Extract dollar amounts
|
|
const amountMatches = text.match(/\$(\d+(?:,\d{3})*(?:\.\d{2})?)/g);
|
|
const amounts =
|
|
amountMatches?.map((match) => parseFloat(match.replace(/[$,]/g, ""))) ||
|
|
[];
|
|
|
|
// Extract monthly payment patterns
|
|
const monthlyMatch = text.match(
|
|
/\$?(\d+(?:,\d{3})*(?:\.\d{2})?)\s*(?:per month|\/month|monthly)/i,
|
|
);
|
|
const monthlyAmount = monthlyMatch
|
|
? parseFloat(monthlyMatch[1].replace(/,/g, ""))
|
|
: undefined;
|
|
|
|
// Extract number of payments/months
|
|
const paymentsMatch = text.match(
|
|
/(\d+)\s*(?:months?|payments?|installments?)/i,
|
|
);
|
|
const numberOfPayments = paymentsMatch
|
|
? parseInt(paymentsMatch[1])
|
|
: undefined;
|
|
|
|
// Extract total amount patterns
|
|
const totalMatch = text.match(
|
|
/(?:total|totaling|total amount)\s*(?:of\s*)?\$?(\d+(?:,\d{3})*(?:\.\d{2})?)/i,
|
|
);
|
|
const totalAmount = totalMatch
|
|
? parseFloat(totalMatch[1].replace(/,/g, ""))
|
|
: undefined;
|
|
|
|
// Determine payment frequency
|
|
let paymentFrequency = undefined;
|
|
if (text.match(/monthly|per month|\/month/i)) paymentFrequency = "monthly";
|
|
else if (text.match(/weekly|per week|\/week/i)) paymentFrequency = "weekly";
|
|
else if (text.match(/bi-weekly|biweekly|every two weeks/i)) {
|
|
paymentFrequency = "bi-weekly";
|
|
}
|
|
|
|
return {
|
|
proposedAmount: amounts.length > 0 ? amounts[0] : undefined,
|
|
monthlyAmount,
|
|
numberOfPayments,
|
|
totalAmount,
|
|
paymentFrequency,
|
|
allAmounts: amounts,
|
|
};
|
|
};
|
|
|
|
const terms = extractFinancialTerms(body);
|
|
|
|
console.log(
|
|
"Fallback Analysis - Extracted Terms:",
|
|
JSON.stringify(terms, null, 2),
|
|
);
|
|
|
|
const result = {
|
|
intent,
|
|
sentiment,
|
|
confidence: 0.6, // Lower confidence for fallback
|
|
extractedTerms: {
|
|
proposedAmount: terms.proposedAmount,
|
|
proposedPaymentPlan: terms.monthlyAmount ? "payment plan" : undefined,
|
|
paymentTerms: (terms.monthlyAmount || terms.numberOfPayments)
|
|
? {
|
|
monthlyAmount: terms.monthlyAmount,
|
|
numberOfPayments: terms.numberOfPayments,
|
|
totalAmount: terms.totalAmount,
|
|
paymentFrequency: terms.paymentFrequency,
|
|
}
|
|
: undefined,
|
|
deadline: undefined,
|
|
conditions: [],
|
|
},
|
|
reasoning:
|
|
`Generated using enhanced keyword-based fallback analysis. Found ${terms.allAmounts.length} amounts.`,
|
|
suggestedNextAction: intent === "acceptance"
|
|
? "mark_settled"
|
|
: intent === "rejection"
|
|
? "escalate_to_user"
|
|
: intent === "counter_offer"
|
|
? "send_counter"
|
|
: "escalate_to_user",
|
|
requiresUserReview: true, // Always require review for fallback
|
|
};
|
|
|
|
console.log("Fallback Analysis Result:", JSON.stringify(result, null, 2));
|
|
return result;
|
|
}
|
|
|
|
serve(async (req) => {
|
|
if (req.method === "OPTIONS") {
|
|
return new Response("ok", { headers: corsHeaders });
|
|
}
|
|
|
|
try {
|
|
const supabaseClient = createClient(
|
|
Deno.env.get("SUPABASE_URL") ?? "",
|
|
Deno.env.get("SUPABASE_ANON_KEY") ?? "",
|
|
{
|
|
global: {
|
|
headers: { Authorization: req.headers.get("Authorization")! },
|
|
},
|
|
},
|
|
);
|
|
|
|
const { debtId, fromEmail, subject, body, messageId }: EmailResponseData =
|
|
await req.json();
|
|
|
|
if (!debtId || !fromEmail || !body) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
error: "Missing required fields: debtId, fromEmail, body",
|
|
}),
|
|
{
|
|
status: 400,
|
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
|
},
|
|
);
|
|
}
|
|
|
|
// Get the debt record and original negotiation context
|
|
const { data: debt, error: debtError } = await supabaseClient
|
|
.from("debts")
|
|
.select("*")
|
|
.eq("id", debtId)
|
|
.single();
|
|
|
|
if (debtError || !debt) {
|
|
return new Response(
|
|
JSON.stringify({ error: "Debt record not found" }),
|
|
{
|
|
status: 404,
|
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
|
},
|
|
);
|
|
}
|
|
|
|
// Analyze the response using AI
|
|
const analysis = await analyzeEmailResponse(
|
|
supabaseClient,
|
|
debtId,
|
|
fromEmail,
|
|
subject,
|
|
body,
|
|
debt.metadata?.aiEmail,
|
|
);
|
|
|
|
console.log({ analysis });
|
|
|
|
// Store the conversation message
|
|
const { error: messageError } = await supabaseClient
|
|
.from("conversation_messages")
|
|
.update({
|
|
debt_id: debtId,
|
|
message_type: analysis.intent === "acceptance"
|
|
? "acceptance"
|
|
: analysis.intent === "rejection"
|
|
? "rejection"
|
|
: analysis.intent === "counter_offer"
|
|
? "counter_offer"
|
|
: "response_received",
|
|
direction: "inbound",
|
|
subject,
|
|
body,
|
|
from_email: fromEmail,
|
|
to_email: debt.metadata?.toEmail || debt.metadata?.fromEmail,
|
|
ai_analysis: analysis,
|
|
})
|
|
.eq("message_id", messageId);
|
|
|
|
if (messageError) {
|
|
console.error("Error storing conversation message:", messageError);
|
|
}
|
|
|
|
// Determine next status and actions based on analysis
|
|
let newStatus = debt.status;
|
|
let newNegotiationRound = debt.negotiation_round || 1;
|
|
let financialOutcome = null;
|
|
let shouldAutoRespond = false;
|
|
let nextAction = null;
|
|
|
|
switch (analysis.intent) {
|
|
case "acceptance":
|
|
newStatus = "accepted";
|
|
nextAction = "mark_settled";
|
|
// Calculate financial outcome when offer is accepted
|
|
financialOutcome = calculateFinancialOutcome(debt, analysis);
|
|
break;
|
|
case "rejection":
|
|
newStatus = "rejected";
|
|
nextAction = "escalate_to_user";
|
|
break;
|
|
case "counter_offer":
|
|
newStatus = "counter_negotiating";
|
|
newNegotiationRound += 1;
|
|
// More conservative auto-response logic for counter-offers
|
|
shouldAutoRespond = !analysis.requiresUserReview &&
|
|
analysis.confidence > 0.9 && // Increased confidence threshold
|
|
analysis.extractedTerms && // Must have specific terms
|
|
(analysis.extractedTerms.proposedAmount ||
|
|
analysis.extractedTerms.paymentTerms) && // Must have concrete financial terms
|
|
body.length > 20; // Must be more than a vague message
|
|
nextAction = analysis.suggestedNextAction;
|
|
break;
|
|
case "request_info":
|
|
newStatus = "requires_manual_review";
|
|
nextAction = "escalate_to_user";
|
|
break;
|
|
default:
|
|
newStatus = "requires_manual_review";
|
|
nextAction = "escalate_to_user";
|
|
}
|
|
|
|
// Update debt record
|
|
const updateData: any = {
|
|
status: newStatus,
|
|
negotiation_round: newNegotiationRound,
|
|
conversation_count: (debt.conversation_count || 0) + 1,
|
|
last_message_at: new Date().toISOString(),
|
|
metadata: {
|
|
...debt.metadata,
|
|
lastResponse: {
|
|
analysis,
|
|
receivedAt: new Date().toISOString(),
|
|
fromEmail,
|
|
subject,
|
|
},
|
|
},
|
|
};
|
|
|
|
// Add financial outcome if offer was accepted
|
|
if (analysis.intent === "acceptance" && financialOutcome) {
|
|
updateData.actual_savings = financialOutcome.actualSavings;
|
|
updateData.status = "settled"; // Set final status here instead of separate update
|
|
updateData.metadata.financialOutcome = {
|
|
...financialOutcome,
|
|
calculatedAt: new Date().toISOString(),
|
|
};
|
|
|
|
// Keep backward compatibility with actualSavings field
|
|
updateData.metadata.actualSavings = {
|
|
amount: financialOutcome.actualSavings,
|
|
calculatedAt: new Date().toISOString(),
|
|
originalAmount: financialOutcome.originalAmount,
|
|
acceptedAmount: financialOutcome.acceptedAmount,
|
|
savingsPercentage: financialOutcome.originalAmount > 0
|
|
? (financialOutcome.actualSavings / financialOutcome.originalAmount *
|
|
100).toFixed(2)
|
|
: 0,
|
|
};
|
|
}
|
|
|
|
const { error: updateError } = await supabaseClient
|
|
.from("debts")
|
|
.update(updateData)
|
|
.eq("id", debtId);
|
|
|
|
if (updateError) {
|
|
console.error("Error updating debt:", updateError);
|
|
}
|
|
|
|
// Log the action
|
|
const auditDetails: any = {
|
|
intent: analysis.intent,
|
|
sentiment: analysis.sentiment,
|
|
confidence: analysis.confidence,
|
|
fromEmail,
|
|
subject,
|
|
suggestedAction: analysis.suggestedNextAction,
|
|
requiresReview: analysis.requiresUserReview,
|
|
};
|
|
|
|
// Add financial outcome to audit log if offer was accepted
|
|
if (analysis.intent === "acceptance" && financialOutcome) {
|
|
auditDetails.financialOutcome = financialOutcome;
|
|
auditDetails.actualSavings = financialOutcome.actualSavings;
|
|
auditDetails.originalAmount = financialOutcome.originalAmount;
|
|
auditDetails.acceptedAmount = financialOutcome.acceptedAmount;
|
|
auditDetails.savingsPercentage = financialOutcome.originalAmount > 0
|
|
? (financialOutcome.actualSavings / financialOutcome.originalAmount *
|
|
100).toFixed(2)
|
|
: 0;
|
|
}
|
|
|
|
await supabaseClient
|
|
.from("audit_logs")
|
|
.insert({
|
|
debt_id: debtId,
|
|
action: analysis.intent === "acceptance"
|
|
? "offer_accepted"
|
|
: "response_analyzed",
|
|
details: {
|
|
...auditDetails,
|
|
nextAction,
|
|
shouldAutoRespond,
|
|
negotiationRound: newNegotiationRound,
|
|
reasoning: analysis.reasoning,
|
|
},
|
|
});
|
|
|
|
// If this is an acceptance, mark as settled
|
|
if (analysis.intent === "acceptance") {
|
|
// await supabaseClient
|
|
// .from("debts")
|
|
// .update({ status: "settled" })
|
|
// .eq("id", debtId);
|
|
|
|
await supabaseClient.from("audit_logs").insert({
|
|
debt_id: debtId,
|
|
action: "debt_settled",
|
|
details: {
|
|
finalAmount: financialOutcome?.acceptedAmount || debt.amount,
|
|
actualSavings: financialOutcome?.actualSavings || 0,
|
|
settlementTerms: analysis.extractedTerms,
|
|
},
|
|
});
|
|
}
|
|
|
|
console.log({
|
|
shouldAutoRespond,
|
|
analysisIntent: analysis.intent,
|
|
analysisConfidence: analysis.confidence,
|
|
});
|
|
|
|
// TEMPORARILY DISABLED: Auto-responses need more testing
|
|
// TODO: Re-enable after thorough testing and user preference settings
|
|
const AUTO_RESPONSES_ENABLED = true;
|
|
const conversationHistory = await getConversationHistory(
|
|
supabaseClient,
|
|
debtId,
|
|
);
|
|
console.log({
|
|
conversationHistoryLength: conversationHistory.length,
|
|
});
|
|
// If auto-response is recommended and confidence is high, trigger negotiation
|
|
if (
|
|
AUTO_RESPONSES_ENABLED &&
|
|
shouldAutoRespond && analysis.confidence > 0.8 &&
|
|
analysis.intent === "counter_offer" &&
|
|
// the length of the conversation isn't bigger than 2 messages
|
|
conversationHistory.length <= 2
|
|
) {
|
|
try {
|
|
const negotiateUrl = `${
|
|
Deno.env.get("SUPABASE_URL")
|
|
}/functions/v1/negotiate`;
|
|
const serviceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY");
|
|
|
|
if (negotiateUrl && serviceKey) {
|
|
await fetch(negotiateUrl, {
|
|
method: "POST",
|
|
headers: {
|
|
Authorization: `Bearer ${serviceKey}`,
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
record: {
|
|
...debt,
|
|
status: newStatus,
|
|
conversation_count: (debt.conversation_count || 0) + 1,
|
|
negotiation_round: newNegotiationRound,
|
|
},
|
|
counterOfferContext: {
|
|
previousResponse: body,
|
|
extractedTerms: analysis.extractedTerms,
|
|
sentiment: analysis.sentiment,
|
|
},
|
|
}),
|
|
});
|
|
|
|
await supabaseClient.from("audit_logs").insert({
|
|
debt_id: debtId,
|
|
action: "auto_counter_triggered",
|
|
details: {
|
|
confidence: analysis.confidence,
|
|
extractedTerms: analysis.extractedTerms,
|
|
},
|
|
});
|
|
}
|
|
} catch (autoResponseError) {
|
|
console.error("Error triggering auto-response:", autoResponseError);
|
|
}
|
|
}
|
|
|
|
const responseData: any = {
|
|
success: true,
|
|
analysis,
|
|
newStatus,
|
|
negotiationRound: newNegotiationRound,
|
|
};
|
|
|
|
// Include financial outcome in response if offer was accepted
|
|
if (analysis.intent === "acceptance" && financialOutcome) {
|
|
responseData.financialOutcome = financialOutcome;
|
|
responseData.actualSavings = financialOutcome.actualSavings;
|
|
responseData.savingsCalculated = true;
|
|
responseData.originalAmount = financialOutcome.originalAmount;
|
|
responseData.acceptedAmount = financialOutcome.acceptedAmount;
|
|
responseData.savingsPercentage = financialOutcome.originalAmount > 0
|
|
? (financialOutcome.actualSavings / financialOutcome.originalAmount *
|
|
100).toFixed(2)
|
|
: 0;
|
|
}
|
|
|
|
return new Response(
|
|
JSON.stringify(responseData),
|
|
{ headers: { ...corsHeaders, "Content-Type": "application/json" } },
|
|
);
|
|
} catch (error) {
|
|
console.error("Error in analyze-response function:", error);
|
|
return new Response(
|
|
JSON.stringify({ error: "Internal server error" }),
|
|
{
|
|
status: 500,
|
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
|
},
|
|
);
|
|
}
|
|
});
|