feat(assets): varlık lotları, ortalama maliyet ve kâr/zarar servisi
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ import { writeAudit } from "@/lib/audit";
|
||||
import { getDb } from "@/lib/db/mongo";
|
||||
import { toObjectId } from "@/lib/object-id";
|
||||
import { AccountModel } from "@/models/account";
|
||||
import { AssetModel } from "@/models/asset";
|
||||
import { ContactModel } from "@/models/contact";
|
||||
import { CategoryModel } from "@/models/category";
|
||||
import { PayeeModel } from "@/models/payee";
|
||||
@@ -70,7 +71,7 @@ export async function loadContext(
|
||||
const m = await resolveMembership(userId, householdId, minRole);
|
||||
if (!m) return null;
|
||||
await postDueInstallments(m.householdId);
|
||||
const [accounts, categories, contacts] = await Promise.all([
|
||||
const [accounts, categories, contacts, assets] = await Promise.all([
|
||||
AccountModel.find({ householdId: m.householdId })
|
||||
.select({ name: 1, type: 1, archivedAt: 1, isPrivate: 1, ownerUserId: 1, icon: 1, color: 1 })
|
||||
.lean(),
|
||||
@@ -89,6 +90,9 @@ export async function loadContext(
|
||||
ContactModel.find({ householdId: m.householdId })
|
||||
.select({ name: 1, isPrivate: 1, ownerUserId: 1, archivedAt: 1 })
|
||||
.lean(),
|
||||
AssetModel.find({ householdId: m.householdId })
|
||||
.select({ name: 1, assetKey: 1, isPrivate: 1, ownerUserId: 1, deletedAt: 1 })
|
||||
.lean(),
|
||||
]);
|
||||
const accountMap = new Map<string, AccountInfo>(
|
||||
accounts.map((a) => [
|
||||
@@ -143,6 +147,19 @@ export async function loadContext(
|
||||
]),
|
||||
);
|
||||
const hiddenContactIds = [...contactMap.values()].filter((c) => c.hidden).map((c) => c.id);
|
||||
const assetMap = new Map(
|
||||
assets.map((a) => [
|
||||
String(a._id),
|
||||
{
|
||||
id: a._id,
|
||||
name: a.name,
|
||||
assetKey: a.assetKey,
|
||||
deleted: Boolean(a.deletedAt),
|
||||
hidden: Boolean(a.isPrivate) && String(a.ownerUserId) !== userId,
|
||||
},
|
||||
]),
|
||||
);
|
||||
const hiddenAssetIds = [...assetMap.values()].filter((a) => a.hidden).map((a) => a.id);
|
||||
return {
|
||||
m,
|
||||
accountMap,
|
||||
@@ -150,13 +167,15 @@ export async function loadContext(
|
||||
contactMap,
|
||||
hiddenAccountIds,
|
||||
hiddenContactIds,
|
||||
assetMap,
|
||||
hiddenAssetIds,
|
||||
flatCategories: flat,
|
||||
};
|
||||
}
|
||||
|
||||
export type Ctx = NonNullable<Awaited<ReturnType<typeof loadContext>>>;
|
||||
|
||||
/** Gizli hesaplara veya gizli carilere dokunan işlemleri dışlayan temel filtre. */
|
||||
/** Gizli hesaplara, gizli carilere veya gizli varlıklara dokunan işlemleri dışlayan temel filtre. */
|
||||
export function baseFilter(ctx: Ctx): Record<string, unknown> {
|
||||
const f: Record<string, unknown> = { householdId: ctx.m.householdId, deletedAt: null };
|
||||
if (ctx.hiddenAccountIds.length) {
|
||||
@@ -164,6 +183,7 @@ export function baseFilter(ctx: Ctx): Record<string, unknown> {
|
||||
f.toAccountId = { $nin: ctx.hiddenAccountIds };
|
||||
}
|
||||
if (ctx.hiddenContactIds.length) f.contactId = { $nin: ctx.hiddenContactIds };
|
||||
if (ctx.hiddenAssetIds.length) f.assetId = { $nin: ctx.hiddenAssetIds };
|
||||
return f;
|
||||
}
|
||||
|
||||
@@ -473,6 +493,7 @@ export async function updateTransaction(
|
||||
if (current.isAdjustment) return fail(t.adjustmentReadOnly);
|
||||
// Cari hareketi cari sayfasındaki formla düzenlenir (tür ve yön cariye özgü).
|
||||
if (current.contactId) return fail(t.contactEditHint);
|
||||
if (current.assetId) return fail(t.assetLocked);
|
||||
const built = buildDoc(ctx, input, current);
|
||||
if (!built.ok) return built;
|
||||
const d = built.data as {
|
||||
@@ -534,6 +555,8 @@ export async function setTransactionDeleted(
|
||||
if (!current || Boolean(current.deletedAt) === deleted) return fail(tr.common.notFound);
|
||||
// Aynî borç ödemesi kaydın ödeme geçmişinin parçasıdır; tek başına silinmez.
|
||||
if (current.inKindRole === "payment") return fail(t.inKindPaymentLocked);
|
||||
// Varlık alım/satışı hareketin parçasıdır; hareket Birikim sayfasından silinir.
|
||||
if (current.assetId) return fail(t.assetLocked);
|
||||
const actor = new Types.ObjectId(actorId);
|
||||
await TransactionModel.updateOne(
|
||||
{ _id: current._id, householdId: ctx.m.householdId },
|
||||
@@ -602,6 +625,8 @@ export type TransactionListItem = {
|
||||
toAccount: { id: string; name: string } | null;
|
||||
/** Cari transferi: kişi ve hareket türü. */
|
||||
contact: { id: string; name: string; kind: ContactEntryKind } | null;
|
||||
/** Varlık alımı/satışı: karşı taraf varlık. */
|
||||
asset: { id: string; name: string } | null;
|
||||
category: { id: string; name: string; path: string; icon: IconKey; color: Swatch } | null;
|
||||
splitCount: number;
|
||||
payeeName: string;
|
||||
@@ -683,6 +708,9 @@ export function toItem(ctx: Ctx, tx: LeanTx, names: Map<string, string>): Transa
|
||||
kind: tx.contactKind,
|
||||
}
|
||||
: null,
|
||||
asset: tx.assetId
|
||||
? { id: String(tx.assetId), name: ctx.assetMap.get(String(tx.assetId))?.name ?? "—" }
|
||||
: null,
|
||||
installment: tx.installment?.planId
|
||||
? {
|
||||
planId: String(tx.installment.planId),
|
||||
|
||||
Reference in New Issue
Block a user