feat(transactions): işlem servisi, bakiye hesaplama, satıcı kuralları ve izolasyon

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-05 03:23:30 +03:00
parent d18c8a0d76
commit 1208e2d1eb
9 changed files with 1394 additions and 11 deletions
+12 -8
View File
@@ -1,6 +1,7 @@
import "server-only";
import { Types } from "mongoose";
import { getHouseholdDetail, resolveMembership } from "@/features/households/service";
import { computeAccountDeltas } from "@/features/transactions/service";
import { tr } from "@/i18n/tr";
import { fail, ok, type ActionResult } from "@/lib/action-result";
import { diffFields, writeAudit } from "@/lib/audit";
@@ -94,12 +95,12 @@ type Lean = {
const canSeeBalance = (a: Lean, viewerId: string) =>
!a.isPrivate || String(a.ownerUserId) === viewerId;
/** FAZ 6b: açılış bakiyesi + işlemler. Şimdilik açılış bakiyesi. */
function computeBalance(a: Lean): number {
return a.openingBalanceMinor;
/** Güncel bakiye = açılış bakiyesi + gerçekleşmiş işlemlerin etkisi (tek doğruluk kaynağı: işlemler). */
function computeBalance(a: Lean, deltas: ReadonlyMap<string, number>): number {
return a.openingBalanceMinor + (deltas.get(String(a._id)) ?? 0);
}
function toView(a: Lean, viewerId: string): AccountView {
function toView(a: Lean, viewerId: string, deltas: ReadonlyMap<string, number>): AccountView {
const visible = canSeeBalance(a, viewerId);
return {
id: String(a._id),
@@ -113,7 +114,7 @@ function toView(a: Lean, viewerId: string): AccountView {
archived: Boolean(a.archivedAt),
isPrivate: a.isPrivate,
ownerUserId: a.ownerUserId ? String(a.ownerUserId) : null,
balanceMinor: visible ? computeBalance(a) : null,
balanceMinor: visible ? computeBalance(a, deltas) : null,
openingBalanceMinor: visible ? a.openingBalanceMinor : null,
openingDate: toIstanbulYmd(a.openingDate),
overdraft: a.overdraft?.enabled ? a.overdraft : null,
@@ -140,7 +141,10 @@ export async function listAccounts(
const rows = (await AccountModel.find(filter).lean<Lean[]>()).sort(
(a, b) => a.order - b.order || compareTr(a.name, b.name),
);
const household = await HouseholdModel.findById(m.householdId).select({ settings: 1 }).lean();
const [household, deltas] = await Promise.all([
HouseholdModel.findById(m.householdId).select({ settings: 1 }).lean(),
computeAccountDeltas(m.householdId),
]);
const includePrivate = household?.settings?.includePrivateAccountsInTotals ?? true;
const totals: AccountTotals = {
@@ -158,12 +162,12 @@ export async function listAccounts(
continue;
}
if (!visible) totals.includesHidden = true;
const balance = computeBalance(a);
const balance = computeBalance(a, deltas);
if (isLiabilityType(a.type)) totals.cardDebtMinor += balance;
else totals.assetsMinor += balance;
}
totals.netMinor = totals.assetsMinor + totals.cardDebtMinor;
return { accounts: rows.map((a) => toView(a, userId)), totals };
return { accounts: rows.map((a) => toView(a, userId, deltas)), totals };
}
/** Form → belge alanları. Kart için açılış borcu negatif saklanır. */