| import { getWriter } from "@ai-sdk-tools/artifacts"; |
| import type { AppContext } from "@api/ai/agents/config/shared"; |
| import { profitArtifact } from "@api/ai/artifacts/profit"; |
| import { generateArtifactDescription } from "@api/ai/utils/artifact-title"; |
| import { resolveToolParams } from "@api/ai/utils/period-dates"; |
| import { checkBankAccountsRequired } from "@api/ai/utils/tool-helpers"; |
| import { db } from "@midday/db/client"; |
| import { getReports } from "@midday/db/queries"; |
| import { formatAmount } from "@midday/utils/format"; |
| import { tool } from "ai"; |
| import { format, parseISO } from "date-fns"; |
| import { z } from "zod"; |
|
|
| const getProfitAnalysisSchema = z.object({ |
| period: z |
| .enum(["3-months", "6-months", "this-year", "1-year", "2-years", "5-years"]) |
| .optional() |
| .describe("Historical period"), |
| from: z.string().optional().describe("Start date (yyyy-MM-dd)"), |
| to: z.string().optional().describe("End date (yyyy-MM-dd)"), |
| currency: z.string().nullable().optional().describe("Currency code"), |
| revenueType: z.enum(["gross", "net"]).optional().describe("Revenue type"), |
| showCanvas: z.boolean().default(false).describe("Show visual canvas"), |
| }); |
|
|
| export const getProfitAnalysisTool = tool({ |
| description: |
| "Analyze profit (revenue minus expenses) - totals, trends, and margins.", |
| inputSchema: getProfitAnalysisSchema, |
| execute: async function* ( |
| { period, from, to, currency, revenueType, showCanvas }, |
| executionOptions, |
| ) { |
| const appContext = executionOptions.experimental_context as AppContext; |
| const teamId = appContext.teamId as string; |
|
|
| if (!teamId) { |
| yield { |
| text: "Unable to retrieve profit analysis: Team ID not found in context.", |
| }; |
| return { |
| currentTotal: 0, |
| prevTotal: 0, |
| currency: currency || appContext.baseCurrency || "USD", |
| monthlyData: [], |
| }; |
| } |
|
|
| const { shouldYield } = checkBankAccountsRequired(appContext); |
| if (shouldYield) { |
| throw new Error("BANK_ACCOUNT_REQUIRED"); |
| } |
|
|
| try { |
| |
| |
| |
| |
| |
| const resolved = resolveToolParams({ |
| toolName: "getProfitAnalysis", |
| appContext, |
| aiParams: { period, from, to, currency, revenueType }, |
| }); |
|
|
| const finalFrom = resolved.from; |
| const finalTo = resolved.to; |
| const finalCurrency = resolved.currency; |
| const finalRevenueType = resolved.revenueType ?? "net"; |
|
|
| |
| const description = generateArtifactDescription(finalFrom, finalTo); |
|
|
| |
| let analysis: ReturnType<typeof profitArtifact.stream> | undefined; |
| if (showCanvas) { |
| const writer = getWriter(executionOptions); |
| analysis = profitArtifact.stream( |
| { |
| stage: "loading", |
| currency: finalCurrency || "USD", |
| from: finalFrom, |
| to: finalTo, |
| description, |
| }, |
| writer, |
| ); |
| } |
|
|
| |
| |
| |
| const [profitResult, revenueResult] = await Promise.all([ |
| getReports(db, { |
| teamId, |
| from: finalFrom, |
| to: finalTo, |
| currency: finalCurrency ?? undefined, |
| type: "profit", |
| revenueType: finalRevenueType, |
| }), |
| getReports(db, { |
| teamId, |
| from: finalFrom, |
| to: finalTo, |
| currency: finalCurrency ?? undefined, |
| type: "revenue", |
| revenueType: "net", |
| }), |
| ]); |
|
|
| const currentTotal = profitResult.summary.currentTotal; |
| const prevTotal = profitResult.summary.prevTotal; |
| const targetCurrency = |
| profitResult.summary.currency || appContext.baseCurrency || "USD"; |
| const locale = appContext.locale || "en-US"; |
| const monthlyData = profitResult.result || []; |
|
|
| |
| const currentRevenueTotal = revenueResult.summary.currentTotal; |
| const prevRevenueTotal = revenueResult.summary.prevTotal; |
| const revenueMonthlyData = revenueResult.result || []; |
|
|
| |
| const formattedCurrentTotal = formatAmount({ |
| amount: Math.abs(currentTotal), |
| currency: targetCurrency, |
| locale, |
| }); |
|
|
| const formattedPrevTotal = formatAmount({ |
| amount: Math.abs(prevTotal), |
| currency: targetCurrency, |
| locale, |
| }); |
|
|
| |
| const changeAmount = currentTotal - prevTotal; |
| const changePercentage = |
| prevTotal !== 0 |
| ? Math.round((changeAmount / Math.abs(prevTotal)) * 100) |
| : 0; |
| const isPositiveChange = changeAmount >= 0; |
|
|
| |
| const last12Months = monthlyData.slice(-12); |
| const last12MonthsRevenue = revenueMonthlyData.slice(-12); |
| const averageProfit = |
| last12Months.length > 0 |
| ? last12Months.reduce((sum, item) => sum + item.current.value, 0) / |
| last12Months.length |
| : 0; |
|
|
| |
| const revenueMap = new Map( |
| last12MonthsRevenue.map((item) => [ |
| item.date, |
| { |
| current: item.current.value, |
| previous: item.previous.value, |
| }, |
| ]), |
| ); |
|
|
| const chartData = last12Months.map((item) => { |
| const revenueData = revenueMap.get(item.date); |
| const revenue = revenueData?.current || 0; |
| const lastYearRevenue = revenueData?.previous || 0; |
| const profit = item.current.value; |
| const lastYearProfit = item.previous.value; |
| |
| const expenses = revenue - profit; |
| const lastYearExpenses = lastYearRevenue - lastYearProfit; |
|
|
| return { |
| month: format(parseISO(item.date), "MMM"), |
| profit, |
| lastYearProfit, |
| average: averageProfit, |
| revenue, |
| expenses, |
| lastYearRevenue, |
| lastYearExpenses, |
| }; |
| }); |
|
|
| |
| if (showCanvas && analysis) { |
| await analysis.update({ |
| stage: "chart_ready", |
| from: finalFrom, |
| to: finalTo, |
| description, |
| chart: { |
| monthlyData: chartData, |
| }, |
| }); |
| } |
|
|
| |
| const currentMonthlyProfit = |
| last12Months.length > 0 |
| ? last12Months[last12Months.length - 1]?.current.value || 0 |
| : 0; |
| const previousMonthlyProfit = |
| last12Months.length > 0 |
| ? last12Months[last12Months.length - 1]?.previous.value || 0 |
| : 0; |
| const currentMonthRevenue = |
| revenueMonthlyData.length > 0 |
| ? revenueMonthlyData[revenueMonthlyData.length - 1]?.current.value || |
| 0 |
| : 0; |
|
|
| |
| |
| const profitMargin = |
| currentRevenueTotal > 0 |
| ? (currentTotal / currentRevenueTotal) * 100 |
| : 0; |
|
|
| |
| const last6Months = last12Months.slice(-6); |
| const averageMonthlyProfit = |
| last6Months.length > 0 |
| ? last6Months.reduce((sum, item) => sum + item.current.value, 0) / |
| last6Months.length |
| : 0; |
|
|
| |
| const revenueGrowth = |
| prevRevenueTotal !== 0 |
| ? Math.round( |
| ((currentRevenueTotal - prevRevenueTotal) / |
| Math.abs(prevRevenueTotal)) * |
| 100, |
| ) |
| : 0; |
|
|
| |
| const previousMonthProfit = |
| last12Months.length > 1 |
| ? last12Months[last12Months.length - 2]?.current.value || 0 |
| : 0; |
| const monthlyProfitChange = |
| previousMonthProfit !== 0 |
| ? Math.round( |
| ((currentMonthlyProfit - previousMonthProfit) / |
| Math.abs(previousMonthProfit)) * |
| 100, |
| ) |
| : 0; |
|
|
| |
| const _currentMonthlyExpenses = |
| currentMonthRevenue - currentMonthlyProfit; |
| const previousMonthRevenue = |
| revenueMonthlyData.length > 0 |
| ? revenueMonthlyData[revenueMonthlyData.length - 1]?.previous.value || |
| 0 |
| : 0; |
| const _previousMonthlyExpenses = |
| previousMonthRevenue - previousMonthlyProfit; |
|
|
| |
| const currentPeriodTotal = { |
| revenue: currentRevenueTotal, |
| expenses: currentRevenueTotal - currentTotal, |
| profit: currentTotal, |
| }; |
|
|
| const previousPeriodTotal = { |
| revenue: prevRevenueTotal, |
| expenses: prevRevenueTotal - prevTotal, |
| profit: prevTotal, |
| }; |
|
|
| |
| if (showCanvas && analysis) { |
| await analysis.update({ |
| stage: "metrics_ready", |
| from: finalFrom, |
| to: finalTo, |
| description, |
| chart: { |
| monthlyData: chartData, |
| }, |
| metrics: { |
| currentMonthlyProfit, |
| profitMargin: Math.round(profitMargin * 10) / 10, |
| averageMonthlyProfit, |
| revenueGrowth, |
| currentMonthlyProfitChange: { |
| percentage: monthlyProfitChange, |
| period: "last month", |
| }, |
| currentPeriod: currentPeriodTotal, |
| previousPeriod: previousPeriodTotal, |
| totalRevenue: currentRevenueTotal, |
| totalExpenses: currentPeriodTotal.expenses, |
| }, |
| }); |
| } |
|
|
| |
| const revenueTypeLabel = finalRevenueType === "gross" ? "Gross" : "Net"; |
| let responseText = `**Total ${revenueTypeLabel} Profit:** ${formattedCurrentTotal}\n\n`; |
|
|
| if (prevTotal !== 0) { |
| responseText += "**Year-over-Year Comparison:**\n"; |
| responseText += `- Current Period: ${formattedCurrentTotal}\n`; |
| responseText += `- Previous Period: ${formattedPrevTotal}\n`; |
| responseText += `- Change: ${isPositiveChange ? "+" : ""}${changePercentage}% (${isPositiveChange ? "+" : ""}${formatAmount( |
| { |
| amount: Math.abs(changeAmount), |
| currency: targetCurrency, |
| locale, |
| }, |
| )})\n\n`; |
| } |
|
|
| |
| if (monthlyData.length > 0) { |
| responseText += "**Monthly Breakdown:**\n\n"; |
| responseText += "| Month | Profit | Change vs Previous Year |\n"; |
| responseText += "|-------|--------|-------------------------|\n"; |
|
|
| for (const item of monthlyData.slice(-12)) { |
| const monthLabel = format(parseISO(item.date), "MMM yyyy"); |
| const profitValue = formatAmount({ |
| amount: Math.abs(item.current.value), |
| currency: targetCurrency, |
| locale, |
| }); |
| const changeValue = item.percentage.value; |
| const changeStatus = item.percentage.status === "positive" ? "+" : ""; |
| responseText += `| ${monthLabel} | ${profitValue} | ${changeStatus}${changeValue.toFixed(1)}% |\n`; |
| } |
| responseText += "\n"; |
| } |
|
|
| |
| if (currentTotal > 0) { |
| responseText += |
| "Your business is profitable, generating more revenue than expenses. "; |
| if (isPositiveChange && prevTotal !== 0) { |
| responseText += |
| "Profitability has improved compared to the previous period, indicating positive business growth."; |
| } else if (!isPositiveChange && prevTotal !== 0) { |
| responseText += |
| "While still profitable, profit has decreased compared to the previous period. Consider reviewing expenses or revenue strategies."; |
| } |
| } else if (currentTotal < 0) { |
| responseText += |
| "Your business is currently operating at a loss (expenses exceed revenue). "; |
| responseText += |
| "This may be normal for growth-stage businesses, but monitor cash flow and runway closely to ensure sustainability."; |
| } else { |
| responseText += |
| "Your business is breaking even (revenue equals expenses). Consider strategies to increase profitability."; |
| } |
|
|
| |
| let summaryText = ""; |
| if (currentTotal > 0) { |
| summaryText = `Profit increased ${Math.abs(changePercentage)}% over ${last12Months.length} months (${formatAmount( |
| { |
| amount: Math.abs(prevTotal), |
| currency: targetCurrency, |
| locale, |
| }, |
| )} to ${formattedCurrentTotal}), driven by revenue growth (${revenueGrowth}% increase). `; |
| summaryText += `Current profit margin of ${Math.round(profitMargin * 10) / 10}% ${ |
| profitMargin >= 25 |
| ? "exceeds industry average of 25%" |
| : profitMargin >= 15 |
| ? "is above average" |
| : "indicates room for improvement" |
| }, indicating ${profitMargin >= 25 ? "strong" : "moderate"} financial performance.`; |
| } else if (currentTotal < 0) { |
| summaryText = |
| "Your business is currently operating at a loss. This may be normal for growth-stage businesses, but monitor cash flow and runway closely."; |
| } else { |
| summaryText = |
| "Your business is breaking even. Consider strategies to increase profitability."; |
| } |
|
|
| |
| if (showCanvas && analysis) { |
| await analysis.update({ |
| stage: "analysis_ready", |
| from: finalFrom, |
| to: finalTo, |
| description, |
| chart: { |
| monthlyData: chartData, |
| }, |
| metrics: { |
| currentMonthlyProfit, |
| profitMargin: Math.round(profitMargin * 10) / 10, |
| averageMonthlyProfit, |
| revenueGrowth, |
| currentMonthlyProfitChange: { |
| percentage: monthlyProfitChange, |
| period: "last month", |
| }, |
| currentPeriod: currentPeriodTotal, |
| previousPeriod: previousPeriodTotal, |
| totalRevenue: currentRevenueTotal, |
| totalExpenses: currentPeriodTotal.expenses, |
| }, |
| analysis: { |
| summary: summaryText, |
| recommendations: [ |
| currentTotal < 0 |
| ? "Focus on reducing expenses or increasing revenue to achieve profitability" |
| : profitMargin < 15 |
| ? "Consider optimizing operations to improve profit margin" |
| : "Maintain current profitability trends and explore growth opportunities", |
| revenueGrowth < 0 |
| ? "Review revenue strategies to reverse declining trends" |
| : "Continue focusing on revenue growth initiatives", |
| ], |
| }, |
| }); |
| } |
|
|
| |
| if (showCanvas) { |
| responseText += |
| "\n\nA detailed visual profit analysis with charts and trends is available."; |
| } |
|
|
| yield { text: responseText }; |
|
|
| return { |
| currentTotal, |
| prevTotal, |
| currency: targetCurrency, |
| changePercentage, |
| changeAmount, |
| monthlyData: monthlyData.map((item) => ({ |
| date: item.date, |
| profit: item.current.value, |
| changePercentage: item.percentage.value, |
| changeStatus: item.percentage.status, |
| })), |
| }; |
| } catch (error) { |
| yield { |
| text: `Failed to retrieve profit analysis: ${error instanceof Error ? error.message : "Unknown error"}`, |
| }; |
| return { |
| currentTotal: 0, |
| prevTotal: 0, |
| currency: currency || appContext.baseCurrency || "USD", |
| monthlyData: [], |
| }; |
| } |
| }, |
| }); |
|
|