feat(assets): dönemsel rapor, net varlık ve aynî kayıt bağlantısı

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-06 00:19:05 +03:00
parent 20e5a90df4
commit 9a60085d14
9 changed files with 806 additions and 19 deletions
+59 -15
View File
@@ -9,6 +9,9 @@ import { toObjectId } from "@/lib/object-id";
import { AssetPriceModel } from "@/models/asset-price";
import { InKindDebtModel } from "@/models/in-kind-debt";
import { TransactionModel } from "@/models/transaction";
import { addMovement } from "@/features/assets/service";
import { AssetMovementModel } from "@/models/asset-movement";
import { priceOn, type PriceQuote } from "./prices";
import {
ASSET_KEYS,
formatAssetQuantity,
@@ -38,19 +41,7 @@ const t = tr.inKind;
// --- Fiyat tablosu ---
export type PriceQuote = { buyMinor: number; sellMinor: number; date: string };
/** Varlığın verilen tarihteki (veya öncesindeki en yakın) fiyatı. */
export async function priceOn(
householdId: Types.ObjectId,
assetKey: AssetKey,
date: string,
): Promise<PriceQuote | null> {
const p = await AssetPriceModel.findOne({ householdId, assetKey, date: { $lte: date } })
.sort({ date: -1 })
.lean();
return p ? { buyMinor: p.buyMinor, sellMinor: p.sellMinor, date: p.date } : null;
}
export { priceOn, type PriceQuote } from "./prices";
export type PriceRow = { assetKey: AssetKey; latest: PriceQuote | null; history: PriceQuote[] };
@@ -381,6 +372,11 @@ export async function deleteInKind(
{ _id: d._id },
{ $set: { deletedAt: new Date(), updatedBy: actor } },
);
// Bağlı varlık hareketleri serbest kalır (Birikim sayfasından yönetilebilir).
await AssetMovementModel.updateMany(
{ householdId: ctx.m.householdId, inKindDebtId: d._id },
{ $set: { inKindDebtId: null } },
);
await writeAudit({
action: "inkind.delete",
actorId: actor,
@@ -491,7 +487,15 @@ export async function recordPayment(
contactName,
);
await mongoose.connection.transaction(async (session) => {
if (input.assetId) {
const asset = ctx.assetMap.get(input.assetId);
if (!asset || asset.hidden || asset.deleted)
return fail(tr.common.notFound, { assetId: tr.common.notFound });
if (asset.assetKey !== d.assetKey)
return fail(t.link.assetMismatch, { assetId: t.link.assetMismatch });
}
const writePayment = async (session: mongoose.ClientSession) => {
let transactionId: Types.ObjectId | null = null;
if (amountMinor > 0 && account) {
const out = d.direction === "debt";
@@ -549,10 +553,50 @@ export async function recordPayment(
},
session,
);
});
};
try {
await mongoose.connection.transaction(async (session) => {
// Varlık bağlantısı: borç ödemesi varlıktan çıkış, alacak tahsilatı varlığa giriş.
if (input.assetId) {
const r = await addMovement(
actorId,
householdId,
input.assetId,
{
kind: d.direction === "debt" ? "gift_out" : "gift_in",
date: input.date,
quantity,
unitPrice: d.direction === "debt" ? null : (d.unitPriceMinor ?? null),
amount: null,
accountId: null,
storage: "",
note: t.link.movementNote(contactName),
},
{ inKindDebtId: d._id, session },
);
if (!r.ok) throw new LinkError(r.error, r.fieldErrors?.quantity);
}
await writePayment(session);
});
} catch (error) {
if (error instanceof LinkError)
return fail(error.message, error.quantity ? { quantity: error.quantity } : undefined);
throw error;
}
return ok({ closed });
}
/** Varlık hareketi eklenemedi: transaction geri alınır, hata kullanıcıya döner. */
class LinkError extends Error {
constructor(
message: string,
readonly quantity?: string,
) {
super(message);
}
}
/** Cari listesi için açık aynî kayıt özetleri (cari kimliği → etiketler). */
export async function openInKindByContact(
userId: string,