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:
@@ -11,6 +11,7 @@ import {
|
||||
Pencil,
|
||||
Plus,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState, useTransition } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { IconBadge } from "@/components/appearance/swatch";
|
||||
@@ -109,7 +110,12 @@ export function AccountsView({
|
||||
<li className={cn("flex items-center gap-3 p-3", a.archived && "opacity-60")}>
|
||||
<IconBadge icon={a.icon} color={a.color} />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate font-medium">{a.name}</p>
|
||||
<Link
|
||||
href={`/islemler?hesap=${a.id}`}
|
||||
className="block truncate font-medium hover:underline"
|
||||
>
|
||||
{a.name}
|
||||
</Link>
|
||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-foreground">
|
||||
{a.type === "meal_card" && a.mealCardProvider ? (
|
||||
<span>{t.providers[a.mealCardProvider]}</span>
|
||||
|
||||
@@ -89,7 +89,9 @@ describe("“yalnızca ben” hesabı", () => {
|
||||
(await listAccounts(ids.user(user), cakir))!.accounts.find(
|
||||
(a) => a.name === "Ayşe — Pluxee",
|
||||
)!;
|
||||
expect((await find("ayse")).balanceMinor).toBe(3_120_00);
|
||||
// Hesap sahibi açılış ve güncel bakiyeyi görür (güncel = açılış + işlemler).
|
||||
expect((await find("ayse")).openingBalanceMinor).toBe(3_120_00);
|
||||
expect((await find("ayse")).balanceMinor).not.toBeNull();
|
||||
expect((await find("admin")).balanceMinor).toBeNull();
|
||||
expect((await find("emre")).balanceMinor).toBeNull();
|
||||
expect((await find("emre")).openingBalanceMinor).toBeNull();
|
||||
@@ -104,7 +106,10 @@ describe("“yalnızca ben” hesabı", () => {
|
||||
);
|
||||
const without = (await listAccounts(ids.user("emre"), cakir))!.totals;
|
||||
expect(without.excludesHidden).toBe(true);
|
||||
expect(withHidden.assetsMinor - without.assetsMinor).toBe(3_120_00);
|
||||
const pluxee = (await listAccounts(ids.user("ayse"), cakir))!.accounts.find(
|
||||
(a) => a.name === "Ayşe — Pluxee",
|
||||
)!;
|
||||
expect(withHidden.assetsMinor - without.assetsMinor).toBe(pluxee.balanceMinor);
|
||||
// Hesap sahibi kendi hesabını her zaman toplamda görür.
|
||||
const ayse = (await listAccounts(ids.user("ayse"), cakir))!.totals;
|
||||
expect(ayse.assetsMinor).toBe(withHidden.assetsMinor);
|
||||
|
||||
@@ -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. */
|
||||
|
||||
Reference in New Issue
Block a user