refactor: replace direct Prisma calls with service layer methods for banking operations

This commit is contained in:
Julien Froidefond
2025-11-27 10:07:03 +01:00
parent c6299de8b2
commit 8ffb65f596
10 changed files with 396 additions and 271 deletions

View 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 },
})
},
}

View 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,
})),
}
},
}

View 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 },
})
},
}

View 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 },
})
},
}

View 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 },
})
},
}