mirror of
https://github.com/marcogll/TaxHacker_s23.git
synced 2026-01-13 13:25:18 +00:00
221 lines
7.3 KiB
TypeScript
221 lines
7.3 KiB
TypeScript
"use server"
|
|
|
|
import { AnalysisResult, analyzeTransaction } from "@/ai/analyze"
|
|
import { AnalyzeAttachment, loadAttachmentsForAI } from "@/ai/attachments"
|
|
import { buildLLMPrompt } from "@/ai/prompt"
|
|
import { fieldsToJsonSchema } from "@/ai/schema"
|
|
import { transactionFormSchema } from "@/forms/transactions"
|
|
import { ActionState } from "@/lib/actions"
|
|
import { getCurrentUser, isAiBalanceExhausted, isSubscriptionExpired } from "@/lib/auth"
|
|
import {
|
|
getDirectorySize,
|
|
getTransactionFileUploadPath,
|
|
getUserUploadsDirectory,
|
|
safePathJoin,
|
|
unsortedFilePath,
|
|
} from "@/lib/files"
|
|
import { DEFAULT_PROMPT_ANALYSE_NEW_FILE } from "@/models/defaults"
|
|
import { createFile, deleteFile, getFileById, updateFile } from "@/models/files"
|
|
import { createTransaction, TransactionData, updateTransactionFiles } from "@/models/transactions"
|
|
import { updateUser } from "@/models/users"
|
|
import { Category, Field, File, Project, Transaction } from "@/prisma/client"
|
|
import { randomUUID } from "crypto"
|
|
import { mkdir, readFile, rename, writeFile } from "fs/promises"
|
|
import { revalidatePath } from "next/cache"
|
|
import path from "path"
|
|
|
|
export async function analyzeFileAction(
|
|
file: File,
|
|
settings: Record<string, string>,
|
|
fields: Field[],
|
|
categories: Category[],
|
|
projects: Project[]
|
|
): Promise<ActionState<AnalysisResult>> {
|
|
const user = await getCurrentUser()
|
|
|
|
if (!file || file.userId !== user.id) {
|
|
return { success: false, error: "File not found or does not belong to the user" }
|
|
}
|
|
|
|
if (isAiBalanceExhausted(user)) {
|
|
return {
|
|
success: false,
|
|
error: "You used all of your pre-paid AI scans, please upgrade your account or buy new subscription plan",
|
|
}
|
|
}
|
|
|
|
if (isSubscriptionExpired(user)) {
|
|
return {
|
|
success: false,
|
|
error: "Your subscription has expired, please upgrade your account or buy new subscription plan",
|
|
}
|
|
}
|
|
|
|
let attachments: AnalyzeAttachment[] = []
|
|
try {
|
|
attachments = await loadAttachmentsForAI(user, file)
|
|
} catch (error) {
|
|
console.error("Failed to retrieve files:", error)
|
|
return { success: false, error: "Failed to retrieve files: " + error }
|
|
}
|
|
|
|
const prompt = buildLLMPrompt(
|
|
settings.prompt_analyse_new_file || DEFAULT_PROMPT_ANALYSE_NEW_FILE,
|
|
fields,
|
|
categories,
|
|
projects
|
|
)
|
|
|
|
const schema = fieldsToJsonSchema(fields)
|
|
|
|
const results = await analyzeTransaction(prompt, schema, attachments, file.id, user.id)
|
|
|
|
console.log("Analysis results:", results)
|
|
|
|
if (results.data?.tokensUsed && results.data.tokensUsed > 0) {
|
|
await updateUser(user.id, { aiBalance: { decrement: 1 } })
|
|
}
|
|
|
|
return results
|
|
}
|
|
|
|
export async function saveFileAsTransactionAction(
|
|
_prevState: ActionState<Transaction> | null,
|
|
formData: FormData
|
|
): Promise<ActionState<Transaction>> {
|
|
try {
|
|
const user = await getCurrentUser()
|
|
const validatedForm = transactionFormSchema.safeParse(Object.fromEntries(formData.entries()))
|
|
|
|
if (!validatedForm.success) {
|
|
return { success: false, error: validatedForm.error.message }
|
|
}
|
|
|
|
// Get the file record
|
|
const fileId = formData.get("fileId") as string
|
|
const file = await getFileById(fileId, user.id)
|
|
if (!file) throw new Error("File not found")
|
|
|
|
// Create transaction
|
|
const transaction = await createTransaction(user.id, validatedForm.data)
|
|
|
|
// Move file to processed location
|
|
const userUploadsDirectory = getUserUploadsDirectory(user)
|
|
const originalFileName = path.basename(file.path)
|
|
const newRelativeFilePath = getTransactionFileUploadPath(file.id, originalFileName, transaction)
|
|
|
|
// Move file to new location and name
|
|
const oldFullFilePath = safePathJoin(userUploadsDirectory, file.path)
|
|
const newFullFilePath = safePathJoin(userUploadsDirectory, newRelativeFilePath)
|
|
await mkdir(path.dirname(newFullFilePath), { recursive: true })
|
|
await rename(path.resolve(oldFullFilePath), path.resolve(newFullFilePath))
|
|
|
|
// Update file record
|
|
await updateFile(file.id, user.id, {
|
|
path: newRelativeFilePath,
|
|
isReviewed: true,
|
|
})
|
|
|
|
await updateTransactionFiles(transaction.id, user.id, [file.id])
|
|
|
|
revalidatePath("/unsorted")
|
|
revalidatePath("/transactions")
|
|
|
|
return { success: true, data: transaction }
|
|
} catch (error) {
|
|
console.error("Failed to save transaction:", error)
|
|
return { success: false, error: `Failed to save transaction: ${error}` }
|
|
}
|
|
}
|
|
|
|
export async function deleteUnsortedFileAction(
|
|
_prevState: ActionState<Transaction> | null,
|
|
fileId: string
|
|
): Promise<ActionState<Transaction>> {
|
|
try {
|
|
const user = await getCurrentUser()
|
|
await deleteFile(fileId, user.id)
|
|
revalidatePath("/unsorted")
|
|
return { success: true }
|
|
} catch (error) {
|
|
console.error("Failed to delete file:", error)
|
|
return { success: false, error: "Failed to delete file" }
|
|
}
|
|
}
|
|
|
|
export async function splitFileIntoItemsAction(
|
|
_prevState: ActionState<null> | null,
|
|
formData: FormData
|
|
): Promise<ActionState<null>> {
|
|
try {
|
|
const user = await getCurrentUser()
|
|
const fileId = formData.get("fileId") as string
|
|
const items = JSON.parse(formData.get("items") as string) as TransactionData[]
|
|
|
|
if (!fileId || !items || items.length === 0) {
|
|
return { success: false, error: "File ID and items are required" }
|
|
}
|
|
|
|
// Get the original file
|
|
const originalFile = await getFileById(fileId, user.id)
|
|
if (!originalFile) {
|
|
return { success: false, error: "Original file not found" }
|
|
}
|
|
|
|
// Get the original file's content
|
|
const userUploadsDirectory = getUserUploadsDirectory(user)
|
|
const originalFilePath = safePathJoin(userUploadsDirectory, originalFile.path)
|
|
const fileContent = await readFile(originalFilePath)
|
|
|
|
// Create a new file for each item
|
|
for (const item of items) {
|
|
const fileUuid = randomUUID()
|
|
const fileName = `${originalFile.filename}-part-${item.name}`
|
|
const relativeFilePath = unsortedFilePath(fileUuid, fileName)
|
|
const fullFilePath = safePathJoin(userUploadsDirectory, relativeFilePath)
|
|
|
|
// Create directory if it doesn't exist
|
|
await mkdir(path.dirname(fullFilePath), { recursive: true })
|
|
|
|
// Copy the original file content
|
|
await writeFile(fullFilePath, fileContent)
|
|
|
|
// Create file record in database with the item data cached
|
|
await createFile(user.id, {
|
|
id: fileUuid,
|
|
filename: fileName,
|
|
path: relativeFilePath,
|
|
mimetype: originalFile.mimetype,
|
|
metadata: originalFile.metadata,
|
|
isSplitted: true,
|
|
cachedParseResult: {
|
|
name: item.name,
|
|
merchant: item.merchant,
|
|
description: item.description,
|
|
total: item.total,
|
|
currencyCode: item.currencyCode,
|
|
categoryCode: item.categoryCode,
|
|
projectCode: item.projectCode,
|
|
type: item.type,
|
|
issuedAt: item.issuedAt,
|
|
note: item.note,
|
|
text: item.text,
|
|
},
|
|
})
|
|
}
|
|
|
|
// Delete the original file
|
|
await deleteFile(fileId, user.id)
|
|
|
|
// Update user storage used
|
|
const storageUsed = await getDirectorySize(getUserUploadsDirectory(user))
|
|
await updateUser(user.id, { storageUsed })
|
|
|
|
revalidatePath("/unsorted")
|
|
return { success: true }
|
|
} catch (error) {
|
|
console.error("Failed to split file into items:", error)
|
|
return { success: false, error: `Failed to split file into items: ${error}` }
|
|
}
|
|
}
|