refactor: replace direct Prisma calls with service layer methods for banking operations
This commit is contained in:
@@ -1,24 +1,11 @@
|
|||||||
import { NextResponse } from "next/server"
|
import { NextResponse } from "next/server"
|
||||||
import { prisma } from "@/lib/prisma"
|
import { accountService } from "@/services/account.service"
|
||||||
import type { Account } from "@/lib/types"
|
import type { Account } from "@/lib/types"
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
const account: Omit<Account, "id"> = await request.json()
|
const data: Omit<Account, "id"> = await request.json()
|
||||||
|
const created = await accountService.create(data)
|
||||||
const created = await prisma.account.create({
|
|
||||||
data: {
|
|
||||||
name: account.name,
|
|
||||||
bankId: account.bankId,
|
|
||||||
accountNumber: account.accountNumber,
|
|
||||||
type: account.type,
|
|
||||||
folderId: account.folderId,
|
|
||||||
balance: account.balance,
|
|
||||||
currency: account.currency,
|
|
||||||
lastImport: account.lastImport,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return NextResponse.json(created)
|
return NextResponse.json(created)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating account:", error)
|
console.error("Error creating account:", error)
|
||||||
@@ -29,21 +16,7 @@ export async function POST(request: Request) {
|
|||||||
export async function PUT(request: Request) {
|
export async function PUT(request: Request) {
|
||||||
try {
|
try {
|
||||||
const account: Account = await request.json()
|
const account: Account = await request.json()
|
||||||
|
const updated = await accountService.update(account.id, account)
|
||||||
const updated = await prisma.account.update({
|
|
||||||
where: { id: account.id },
|
|
||||||
data: {
|
|
||||||
name: account.name,
|
|
||||||
bankId: account.bankId,
|
|
||||||
accountNumber: account.accountNumber,
|
|
||||||
type: account.type,
|
|
||||||
folderId: account.folderId,
|
|
||||||
balance: account.balance,
|
|
||||||
currency: account.currency,
|
|
||||||
lastImport: account.lastImport,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return NextResponse.json(updated)
|
return NextResponse.json(updated)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating account:", error)
|
console.error("Error updating account:", error)
|
||||||
@@ -60,15 +33,10 @@ export async function DELETE(request: Request) {
|
|||||||
return NextResponse.json({ error: "Account ID is required" }, { status: 400 })
|
return NextResponse.json({ error: "Account ID is required" }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transactions will be deleted automatically due to onDelete: Cascade
|
await accountService.delete(id)
|
||||||
await prisma.account.delete({
|
|
||||||
where: { id },
|
|
||||||
})
|
|
||||||
|
|
||||||
return NextResponse.json({ success: true })
|
return NextResponse.json({ success: true })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error deleting account:", error)
|
console.error("Error deleting account:", error)
|
||||||
return NextResponse.json({ error: "Failed to delete account" }, { status: 500 })
|
return NextResponse.json({ error: "Failed to delete account" }, { status: 500 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,12 @@
|
|||||||
import { NextResponse } from "next/server"
|
import { NextResponse } from "next/server"
|
||||||
import { prisma } from "@/lib/prisma"
|
import { categoryService } from "@/services/category.service"
|
||||||
import type { Category } from "@/lib/types"
|
import type { Category } from "@/lib/types"
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
const category: Omit<Category, "id"> = await request.json()
|
const data: Omit<Category, "id"> = await request.json()
|
||||||
|
const created = await categoryService.create(data)
|
||||||
const created = await prisma.category.create({
|
return NextResponse.json(created)
|
||||||
data: {
|
|
||||||
name: category.name,
|
|
||||||
color: category.color,
|
|
||||||
icon: category.icon,
|
|
||||||
keywords: JSON.stringify(category.keywords),
|
|
||||||
parentId: category.parentId,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
...created,
|
|
||||||
keywords: JSON.parse(created.keywords),
|
|
||||||
})
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating category:", error)
|
console.error("Error creating category:", error)
|
||||||
return NextResponse.json({ error: "Failed to create category" }, { status: 500 })
|
return NextResponse.json({ error: "Failed to create category" }, { status: 500 })
|
||||||
@@ -29,22 +16,8 @@ export async function POST(request: Request) {
|
|||||||
export async function PUT(request: Request) {
|
export async function PUT(request: Request) {
|
||||||
try {
|
try {
|
||||||
const category: Category = await request.json()
|
const category: Category = await request.json()
|
||||||
|
const updated = await categoryService.update(category.id, category)
|
||||||
const updated = await prisma.category.update({
|
return NextResponse.json(updated)
|
||||||
where: { id: category.id },
|
|
||||||
data: {
|
|
||||||
name: category.name,
|
|
||||||
color: category.color,
|
|
||||||
icon: category.icon,
|
|
||||||
keywords: JSON.stringify(category.keywords),
|
|
||||||
parentId: category.parentId,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
...updated,
|
|
||||||
keywords: JSON.parse(updated.keywords),
|
|
||||||
})
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating category:", error)
|
console.error("Error updating category:", error)
|
||||||
return NextResponse.json({ error: "Failed to update category" }, { status: 500 })
|
return NextResponse.json({ error: "Failed to update category" }, { status: 500 })
|
||||||
@@ -60,20 +33,10 @@ export async function DELETE(request: Request) {
|
|||||||
return NextResponse.json({ error: "Category ID is required" }, { status: 400 })
|
return NextResponse.json({ error: "Category ID is required" }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove category from transactions (set to null)
|
await categoryService.delete(id)
|
||||||
await prisma.transaction.updateMany({
|
|
||||||
where: { categoryId: id },
|
|
||||||
data: { categoryId: null },
|
|
||||||
})
|
|
||||||
|
|
||||||
await prisma.category.delete({
|
|
||||||
where: { id },
|
|
||||||
})
|
|
||||||
|
|
||||||
return NextResponse.json({ success: true })
|
return NextResponse.json({ success: true })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error deleting category:", error)
|
console.error("Error deleting category:", error)
|
||||||
return NextResponse.json({ error: "Failed to delete category" }, { status: 500 })
|
return NextResponse.json({ error: "Failed to delete category" }, { status: 500 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,11 @@
|
|||||||
import { NextResponse } from "next/server"
|
import { NextResponse } from "next/server"
|
||||||
import { prisma } from "@/lib/prisma"
|
import { folderService, FolderNotFoundError } from "@/services/folder.service"
|
||||||
import type { Folder } from "@/lib/types"
|
import type { Folder } from "@/lib/types"
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
const folder: Omit<Folder, "id"> = await request.json()
|
const data: Omit<Folder, "id"> = await request.json()
|
||||||
|
const created = await folderService.create(data)
|
||||||
const created = await prisma.folder.create({
|
|
||||||
data: {
|
|
||||||
name: folder.name,
|
|
||||||
parentId: folder.parentId,
|
|
||||||
color: folder.color,
|
|
||||||
icon: folder.icon,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return NextResponse.json(created)
|
return NextResponse.json(created)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating folder:", error)
|
console.error("Error creating folder:", error)
|
||||||
@@ -25,17 +16,7 @@ export async function POST(request: Request) {
|
|||||||
export async function PUT(request: Request) {
|
export async function PUT(request: Request) {
|
||||||
try {
|
try {
|
||||||
const folder: Folder = await request.json()
|
const folder: Folder = await request.json()
|
||||||
|
const updated = await folderService.update(folder.id, folder)
|
||||||
const updated = await prisma.folder.update({
|
|
||||||
where: { id: folder.id },
|
|
||||||
data: {
|
|
||||||
name: folder.name,
|
|
||||||
parentId: folder.parentId,
|
|
||||||
color: folder.color,
|
|
||||||
icon: folder.icon,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return NextResponse.json(updated)
|
return NextResponse.json(updated)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating folder:", error)
|
console.error("Error updating folder:", error)
|
||||||
@@ -52,43 +33,13 @@ export async function DELETE(request: Request) {
|
|||||||
return NextResponse.json({ error: "Folder ID is required" }, { status: 400 })
|
return NextResponse.json({ error: "Folder ID is required" }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
const folder = await prisma.folder.findUnique({
|
await folderService.delete(id)
|
||||||
where: { id },
|
|
||||||
include: { children: true },
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!folder) {
|
|
||||||
return NextResponse.json({ error: "Folder not found" }, { status: 404 })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move accounts to root (null)
|
|
||||||
await prisma.account.updateMany({
|
|
||||||
where: { folderId: id },
|
|
||||||
data: { folderId: null },
|
|
||||||
})
|
|
||||||
|
|
||||||
// Move subfolders to parent
|
|
||||||
if (folder.parentId) {
|
|
||||||
await prisma.folder.updateMany({
|
|
||||||
where: { parentId: id },
|
|
||||||
data: { parentId: folder.parentId },
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// If no parent, move to null (root)
|
|
||||||
await prisma.folder.updateMany({
|
|
||||||
where: { parentId: id },
|
|
||||||
data: { parentId: null },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.folder.delete({
|
|
||||||
where: { id },
|
|
||||||
})
|
|
||||||
|
|
||||||
return NextResponse.json({ success: true })
|
return NextResponse.json({ success: true })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error instanceof FolderNotFoundError) {
|
||||||
|
return NextResponse.json({ error: "Folder not found" }, { status: 404 })
|
||||||
|
}
|
||||||
console.error("Error deleting folder:", error)
|
console.error("Error deleting folder:", error)
|
||||||
return NextResponse.json({ error: "Failed to delete folder" }, { status: 500 })
|
return NextResponse.json({ error: "Failed to delete folder" }, { status: 500 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,79 +1,12 @@
|
|||||||
import { NextResponse } from "next/server"
|
import { NextResponse } from "next/server"
|
||||||
import { prisma } from "@/lib/prisma"
|
import { bankingService } from "@/services/banking.service"
|
||||||
import type { BankingData } from "@/lib/types"
|
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
const [accounts, transactions, folders, categories, categoryRules] = await Promise.all([
|
const data = await bankingService.getAllData()
|
||||||
prisma.account.findMany({
|
|
||||||
include: {
|
|
||||||
folder: true,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
prisma.transaction.findMany({
|
|
||||||
include: {
|
|
||||||
account: true,
|
|
||||||
category: true,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
prisma.folder.findMany(),
|
|
||||||
prisma.category.findMany(),
|
|
||||||
prisma.categoryRule.findMany(),
|
|
||||||
])
|
|
||||||
|
|
||||||
// Transform Prisma models to match our types
|
|
||||||
const data: BankingData = {
|
|
||||||
accounts: accounts.map((a) => ({
|
|
||||||
id: a.id,
|
|
||||||
name: a.name,
|
|
||||||
bankId: a.bankId,
|
|
||||||
accountNumber: a.accountNumber,
|
|
||||||
type: a.type as "CHECKING" | "SAVINGS" | "CREDIT_CARD" | "OTHER",
|
|
||||||
folderId: a.folderId,
|
|
||||||
balance: a.balance,
|
|
||||||
currency: a.currency,
|
|
||||||
lastImport: a.lastImport,
|
|
||||||
})),
|
|
||||||
transactions: transactions.map((t) => ({
|
|
||||||
id: t.id,
|
|
||||||
accountId: t.accountId,
|
|
||||||
date: t.date,
|
|
||||||
amount: t.amount,
|
|
||||||
description: t.description,
|
|
||||||
type: t.type as "DEBIT" | "CREDIT",
|
|
||||||
categoryId: t.categoryId,
|
|
||||||
isReconciled: t.isReconciled,
|
|
||||||
fitId: t.fitId,
|
|
||||||
memo: t.memo ?? undefined,
|
|
||||||
checkNum: t.checkNum ?? undefined,
|
|
||||||
})),
|
|
||||||
folders: folders.map((f) => ({
|
|
||||||
id: f.id,
|
|
||||||
name: f.name,
|
|
||||||
parentId: f.parentId,
|
|
||||||
color: f.color,
|
|
||||||
icon: f.icon,
|
|
||||||
})),
|
|
||||||
categories: categories.map((c) => ({
|
|
||||||
id: c.id,
|
|
||||||
name: c.name,
|
|
||||||
color: c.color,
|
|
||||||
icon: c.icon,
|
|
||||||
keywords: JSON.parse(c.keywords) as string[],
|
|
||||||
parentId: c.parentId,
|
|
||||||
})),
|
|
||||||
categoryRules: categoryRules.map((r) => ({
|
|
||||||
id: r.id,
|
|
||||||
categoryId: r.categoryId,
|
|
||||||
pattern: r.pattern,
|
|
||||||
isRegex: r.isRegex,
|
|
||||||
})),
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json(data)
|
return NextResponse.json(data)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching banking data:", error)
|
console.error("Error fetching banking data:", error)
|
||||||
return NextResponse.json({ error: "Failed to fetch data" }, { status: 500 })
|
return NextResponse.json({ error: "Failed to fetch data" }, { status: 500 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,51 +1,12 @@
|
|||||||
import { NextResponse } from "next/server"
|
import { NextResponse } from "next/server"
|
||||||
import { prisma } from "@/lib/prisma"
|
import { transactionService } from "@/services/transaction.service"
|
||||||
import type { Transaction } from "@/lib/types"
|
import type { Transaction } from "@/lib/types"
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
const transactions: Transaction[] = await request.json()
|
const transactions: Transaction[] = await request.json()
|
||||||
|
const result = await transactionService.createMany(transactions)
|
||||||
// Filter out duplicates based on fitId
|
return NextResponse.json(result)
|
||||||
const existingTransactions = await prisma.transaction.findMany({
|
|
||||||
where: {
|
|
||||||
accountId: { in: transactions.map((t) => t.accountId) },
|
|
||||||
fitId: { in: transactions.map((t) => t.fitId) },
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
accountId: true,
|
|
||||||
fitId: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const existingSet = new Set(
|
|
||||||
existingTransactions.map((t) => `${t.accountId}-${t.fitId}`),
|
|
||||||
)
|
|
||||||
|
|
||||||
const newTransactions = transactions.filter(
|
|
||||||
(t) => !existingSet.has(`${t.accountId}-${t.fitId}`),
|
|
||||||
)
|
|
||||||
|
|
||||||
if (newTransactions.length === 0) {
|
|
||||||
return NextResponse.json({ count: 0, transactions: [] })
|
|
||||||
}
|
|
||||||
|
|
||||||
const created = await prisma.transaction.createMany({
|
|
||||||
data: newTransactions.map((t) => ({
|
|
||||||
accountId: t.accountId,
|
|
||||||
date: t.date,
|
|
||||||
amount: t.amount,
|
|
||||||
description: t.description,
|
|
||||||
type: t.type,
|
|
||||||
categoryId: t.categoryId,
|
|
||||||
isReconciled: t.isReconciled,
|
|
||||||
fitId: t.fitId,
|
|
||||||
memo: t.memo,
|
|
||||||
checkNum: t.checkNum,
|
|
||||||
})),
|
|
||||||
})
|
|
||||||
|
|
||||||
return NextResponse.json({ count: created.count, transactions: newTransactions })
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating transactions:", error)
|
console.error("Error creating transactions:", error)
|
||||||
return NextResponse.json({ error: "Failed to create transactions" }, { status: 500 })
|
return NextResponse.json({ error: "Failed to create transactions" }, { status: 500 })
|
||||||
@@ -55,23 +16,7 @@ export async function POST(request: Request) {
|
|||||||
export async function PUT(request: Request) {
|
export async function PUT(request: Request) {
|
||||||
try {
|
try {
|
||||||
const transaction: Transaction = await request.json()
|
const transaction: Transaction = await request.json()
|
||||||
|
const updated = await transactionService.update(transaction.id, transaction)
|
||||||
const updated = await prisma.transaction.update({
|
|
||||||
where: { id: transaction.id },
|
|
||||||
data: {
|
|
||||||
accountId: transaction.accountId,
|
|
||||||
date: transaction.date,
|
|
||||||
amount: transaction.amount,
|
|
||||||
description: transaction.description,
|
|
||||||
type: transaction.type,
|
|
||||||
categoryId: transaction.categoryId,
|
|
||||||
isReconciled: transaction.isReconciled,
|
|
||||||
fitId: transaction.fitId,
|
|
||||||
memo: transaction.memo,
|
|
||||||
checkNum: transaction.checkNum,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return NextResponse.json(updated)
|
return NextResponse.json(updated)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating transaction:", error)
|
console.error("Error updating transaction:", error)
|
||||||
@@ -88,14 +33,10 @@ export async function DELETE(request: Request) {
|
|||||||
return NextResponse.json({ error: "Transaction ID is required" }, { status: 400 })
|
return NextResponse.json({ error: "Transaction ID is required" }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.transaction.delete({
|
await transactionService.delete(id)
|
||||||
where: { id },
|
|
||||||
})
|
|
||||||
|
|
||||||
return NextResponse.json({ success: true })
|
return NextResponse.json({ success: true })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error deleting transaction:", error)
|
console.error("Error deleting transaction:", error)
|
||||||
return NextResponse.json({ error: "Failed to delete transaction" }, { status: 500 })
|
return NextResponse.json({ error: "Failed to delete transaction" }, { status: 500 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
67
services/account.service.ts
Normal file
67
services/account.service.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { prisma } from "@/lib/prisma"
|
||||||
|
import type { Account } from "@/lib/types"
|
||||||
|
|
||||||
|
export const accountService = {
|
||||||
|
async create(data: Omit<Account, "id">): Promise<Account> {
|
||||||
|
const created = await prisma.account.create({
|
||||||
|
data: {
|
||||||
|
name: data.name,
|
||||||
|
bankId: data.bankId,
|
||||||
|
accountNumber: data.accountNumber,
|
||||||
|
type: data.type,
|
||||||
|
folderId: data.folderId,
|
||||||
|
balance: data.balance,
|
||||||
|
currency: data.currency,
|
||||||
|
lastImport: data.lastImport,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: created.id,
|
||||||
|
name: created.name,
|
||||||
|
bankId: created.bankId,
|
||||||
|
accountNumber: created.accountNumber,
|
||||||
|
type: created.type as Account["type"],
|
||||||
|
folderId: created.folderId,
|
||||||
|
balance: created.balance,
|
||||||
|
currency: created.currency,
|
||||||
|
lastImport: created.lastImport,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async update(id: string, data: Partial<Omit<Account, "id">>): Promise<Account> {
|
||||||
|
const updated = await prisma.account.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
name: data.name,
|
||||||
|
bankId: data.bankId,
|
||||||
|
accountNumber: data.accountNumber,
|
||||||
|
type: data.type,
|
||||||
|
folderId: data.folderId,
|
||||||
|
balance: data.balance,
|
||||||
|
currency: data.currency,
|
||||||
|
lastImport: data.lastImport,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: updated.id,
|
||||||
|
name: updated.name,
|
||||||
|
bankId: updated.bankId,
|
||||||
|
accountNumber: updated.accountNumber,
|
||||||
|
type: updated.type as Account["type"],
|
||||||
|
folderId: updated.folderId,
|
||||||
|
balance: updated.balance,
|
||||||
|
currency: updated.currency,
|
||||||
|
lastImport: updated.lastImport,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async delete(id: string): Promise<void> {
|
||||||
|
// Transactions will be deleted automatically due to onDelete: Cascade
|
||||||
|
await prisma.account.delete({
|
||||||
|
where: { id },
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
73
services/banking.service.ts
Normal file
73
services/banking.service.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { prisma } from "@/lib/prisma"
|
||||||
|
import type { BankingData, Account, Transaction, Folder, Category, CategoryRule } from "@/lib/types"
|
||||||
|
|
||||||
|
export const bankingService = {
|
||||||
|
async getAllData(): Promise<BankingData> {
|
||||||
|
const [accounts, transactions, folders, categories, categoryRules] = await Promise.all([
|
||||||
|
prisma.account.findMany({
|
||||||
|
include: {
|
||||||
|
folder: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.transaction.findMany({
|
||||||
|
include: {
|
||||||
|
account: true,
|
||||||
|
category: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.folder.findMany(),
|
||||||
|
prisma.category.findMany(),
|
||||||
|
prisma.categoryRule.findMany(),
|
||||||
|
])
|
||||||
|
|
||||||
|
// Transform Prisma models to match our types
|
||||||
|
return {
|
||||||
|
accounts: accounts.map((a): Account => ({
|
||||||
|
id: a.id,
|
||||||
|
name: a.name,
|
||||||
|
bankId: a.bankId,
|
||||||
|
accountNumber: a.accountNumber,
|
||||||
|
type: a.type as Account["type"],
|
||||||
|
folderId: a.folderId,
|
||||||
|
balance: a.balance,
|
||||||
|
currency: a.currency,
|
||||||
|
lastImport: a.lastImport,
|
||||||
|
})),
|
||||||
|
transactions: transactions.map((t): Transaction => ({
|
||||||
|
id: t.id,
|
||||||
|
accountId: t.accountId,
|
||||||
|
date: t.date,
|
||||||
|
amount: t.amount,
|
||||||
|
description: t.description,
|
||||||
|
type: t.type as Transaction["type"],
|
||||||
|
categoryId: t.categoryId,
|
||||||
|
isReconciled: t.isReconciled,
|
||||||
|
fitId: t.fitId,
|
||||||
|
memo: t.memo ?? undefined,
|
||||||
|
checkNum: t.checkNum ?? undefined,
|
||||||
|
})),
|
||||||
|
folders: folders.map((f): Folder => ({
|
||||||
|
id: f.id,
|
||||||
|
name: f.name,
|
||||||
|
parentId: f.parentId,
|
||||||
|
color: f.color,
|
||||||
|
icon: f.icon,
|
||||||
|
})),
|
||||||
|
categories: categories.map((c): Category => ({
|
||||||
|
id: c.id,
|
||||||
|
name: c.name,
|
||||||
|
color: c.color,
|
||||||
|
icon: c.icon,
|
||||||
|
keywords: JSON.parse(c.keywords) as string[],
|
||||||
|
parentId: c.parentId,
|
||||||
|
})),
|
||||||
|
categoryRules: categoryRules.map((r): CategoryRule => ({
|
||||||
|
id: r.id,
|
||||||
|
categoryId: r.categoryId,
|
||||||
|
pattern: r.pattern,
|
||||||
|
isRegex: r.isRegex,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
60
services/category.service.ts
Normal file
60
services/category.service.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { prisma } from "@/lib/prisma"
|
||||||
|
import type { Category } from "@/lib/types"
|
||||||
|
|
||||||
|
export const categoryService = {
|
||||||
|
async create(data: Omit<Category, "id">): Promise<Category> {
|
||||||
|
const created = await prisma.category.create({
|
||||||
|
data: {
|
||||||
|
name: data.name,
|
||||||
|
color: data.color,
|
||||||
|
icon: data.icon,
|
||||||
|
keywords: JSON.stringify(data.keywords),
|
||||||
|
parentId: data.parentId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: created.id,
|
||||||
|
name: created.name,
|
||||||
|
color: created.color,
|
||||||
|
icon: created.icon,
|
||||||
|
keywords: JSON.parse(created.keywords) as string[],
|
||||||
|
parentId: created.parentId,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async update(id: string, data: Partial<Omit<Category, "id">>): Promise<Category> {
|
||||||
|
const updated = await prisma.category.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
name: data.name,
|
||||||
|
color: data.color,
|
||||||
|
icon: data.icon,
|
||||||
|
keywords: data.keywords ? JSON.stringify(data.keywords) : undefined,
|
||||||
|
parentId: data.parentId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: updated.id,
|
||||||
|
name: updated.name,
|
||||||
|
color: updated.color,
|
||||||
|
icon: updated.icon,
|
||||||
|
keywords: JSON.parse(updated.keywords) as string[],
|
||||||
|
parentId: updated.parentId,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async delete(id: string): Promise<void> {
|
||||||
|
// Business rule: Remove category from transactions (set to null)
|
||||||
|
await prisma.transaction.updateMany({
|
||||||
|
where: { categoryId: id },
|
||||||
|
data: { categoryId: null },
|
||||||
|
})
|
||||||
|
|
||||||
|
await prisma.category.delete({
|
||||||
|
where: { id },
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
78
services/folder.service.ts
Normal file
78
services/folder.service.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { prisma } from "@/lib/prisma"
|
||||||
|
import type { Folder } from "@/lib/types"
|
||||||
|
|
||||||
|
export class FolderNotFoundError extends Error {
|
||||||
|
constructor(id: string) {
|
||||||
|
super(`Folder not found: ${id}`)
|
||||||
|
this.name = "FolderNotFoundError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const folderService = {
|
||||||
|
async create(data: Omit<Folder, "id">): Promise<Folder> {
|
||||||
|
const created = await prisma.folder.create({
|
||||||
|
data: {
|
||||||
|
name: data.name,
|
||||||
|
parentId: data.parentId,
|
||||||
|
color: data.color,
|
||||||
|
icon: data.icon,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: created.id,
|
||||||
|
name: created.name,
|
||||||
|
parentId: created.parentId,
|
||||||
|
color: created.color,
|
||||||
|
icon: created.icon,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async update(id: string, data: Partial<Omit<Folder, "id">>): Promise<Folder> {
|
||||||
|
const updated = await prisma.folder.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
name: data.name,
|
||||||
|
parentId: data.parentId,
|
||||||
|
color: data.color,
|
||||||
|
icon: data.icon,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: updated.id,
|
||||||
|
name: updated.name,
|
||||||
|
parentId: updated.parentId,
|
||||||
|
color: updated.color,
|
||||||
|
icon: updated.icon,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async delete(id: string): Promise<void> {
|
||||||
|
const folder = await prisma.folder.findUnique({
|
||||||
|
where: { id },
|
||||||
|
include: { children: true },
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!folder) {
|
||||||
|
throw new FolderNotFoundError(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Business rule: Move accounts to root (null folderId)
|
||||||
|
await prisma.account.updateMany({
|
||||||
|
where: { folderId: id },
|
||||||
|
data: { folderId: null },
|
||||||
|
})
|
||||||
|
|
||||||
|
// Business rule: Move subfolders to parent (or root if no parent)
|
||||||
|
await prisma.folder.updateMany({
|
||||||
|
where: { parentId: id },
|
||||||
|
data: { parentId: folder.parentId },
|
||||||
|
})
|
||||||
|
|
||||||
|
await prisma.folder.delete({
|
||||||
|
where: { id },
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
91
services/transaction.service.ts
Normal file
91
services/transaction.service.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import { prisma } from "@/lib/prisma"
|
||||||
|
import type { Transaction } from "@/lib/types"
|
||||||
|
|
||||||
|
export interface CreateManyResult {
|
||||||
|
count: number
|
||||||
|
transactions: Transaction[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const transactionService = {
|
||||||
|
async createMany(transactions: Transaction[]): Promise<CreateManyResult> {
|
||||||
|
// Filter out duplicates based on fitId (business rule)
|
||||||
|
const existingTransactions = await prisma.transaction.findMany({
|
||||||
|
where: {
|
||||||
|
accountId: { in: transactions.map((t) => t.accountId) },
|
||||||
|
fitId: { in: transactions.map((t) => t.fitId) },
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
accountId: true,
|
||||||
|
fitId: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const existingSet = new Set(
|
||||||
|
existingTransactions.map((t) => `${t.accountId}-${t.fitId}`),
|
||||||
|
)
|
||||||
|
|
||||||
|
const newTransactions = transactions.filter(
|
||||||
|
(t) => !existingSet.has(`${t.accountId}-${t.fitId}`),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (newTransactions.length === 0) {
|
||||||
|
return { count: 0, transactions: [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
const created = await prisma.transaction.createMany({
|
||||||
|
data: newTransactions.map((t) => ({
|
||||||
|
accountId: t.accountId,
|
||||||
|
date: t.date,
|
||||||
|
amount: t.amount,
|
||||||
|
description: t.description,
|
||||||
|
type: t.type,
|
||||||
|
categoryId: t.categoryId,
|
||||||
|
isReconciled: t.isReconciled,
|
||||||
|
fitId: t.fitId,
|
||||||
|
memo: t.memo,
|
||||||
|
checkNum: t.checkNum,
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
|
||||||
|
return { count: created.count, transactions: newTransactions }
|
||||||
|
},
|
||||||
|
|
||||||
|
async update(id: string, data: Partial<Omit<Transaction, "id">>): Promise<Transaction> {
|
||||||
|
const updated = await prisma.transaction.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
accountId: data.accountId,
|
||||||
|
date: data.date,
|
||||||
|
amount: data.amount,
|
||||||
|
description: data.description,
|
||||||
|
type: data.type,
|
||||||
|
categoryId: data.categoryId,
|
||||||
|
isReconciled: data.isReconciled,
|
||||||
|
fitId: data.fitId,
|
||||||
|
memo: data.memo,
|
||||||
|
checkNum: data.checkNum,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: updated.id,
|
||||||
|
accountId: updated.accountId,
|
||||||
|
date: updated.date,
|
||||||
|
amount: updated.amount,
|
||||||
|
description: updated.description,
|
||||||
|
type: updated.type as Transaction["type"],
|
||||||
|
categoryId: updated.categoryId,
|
||||||
|
isReconciled: updated.isReconciled,
|
||||||
|
fitId: updated.fitId,
|
||||||
|
memo: updated.memo ?? undefined,
|
||||||
|
checkNum: updated.checkNum ?? undefined,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async delete(id: string): Promise<void> {
|
||||||
|
await prisma.transaction.delete({
|
||||||
|
where: { id },
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user