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:
@@ -1,7 +1,7 @@
|
||||
import "server-only";
|
||||
import mongoose, { Types } from "mongoose";
|
||||
import { hasHouseholdRole } from "@/features/households/roles";
|
||||
import { priceOn, type PriceQuote } from "@/features/in-kind/service";
|
||||
import { buyPriceLookup, priceOn, type PriceQuote } from "@/features/in-kind/prices";
|
||||
import { loadContext, type Ctx } from "@/features/transactions/service";
|
||||
import { tr } from "@/i18n/tr";
|
||||
import { fail, ok, type ActionResult } from "@/lib/action-result";
|
||||
@@ -14,10 +14,14 @@ import { formatAssetQuantity, valueOf, type AssetKey } from "@/shared/assets";
|
||||
import { istanbulDayStart, todayIstanbul } from "@/shared/dates";
|
||||
import {
|
||||
holdingSummary,
|
||||
periodReport,
|
||||
runMovements,
|
||||
sumReports,
|
||||
type HoldingSummary,
|
||||
type MovementKind,
|
||||
type PeriodReport,
|
||||
} from "@/shared/portfolio";
|
||||
import { addDays, addMonths } from "@/shared/recurrence";
|
||||
import { QUANTITY_SCALE } from "@/shared/quantity";
|
||||
import type { AssetForm, MovementForm } from "@/shared/schemas/asset";
|
||||
import { compareTr, foldTr } from "@/shared/search";
|
||||
@@ -482,3 +486,58 @@ export async function deleteMovement(
|
||||
});
|
||||
return ok(undefined);
|
||||
}
|
||||
|
||||
// --- Dönemsel rapor ---
|
||||
|
||||
export const REPORT_RANGES = ["3", "6", "12", "tum"] as const;
|
||||
export type ReportRange = (typeof REPORT_RANGES)[number];
|
||||
|
||||
/**
|
||||
* Seçilen aralıkta (son N ay veya ilk hareketten bugüne) yatırılan TL, dönem sonu değer,
|
||||
* kâr/zarar ve aylık değer noktaları. `assetId` verilirse yalnızca o varlık, yoksa tüm görünür
|
||||
* varlıkların toplamı.
|
||||
*/
|
||||
export async function getPeriodReport(
|
||||
userId: string,
|
||||
householdId: string,
|
||||
range: ReportRange,
|
||||
assetId?: string,
|
||||
): Promise<(PeriodReport & { excluded: number }) | null> {
|
||||
const ctx = await loadContext(userId, householdId, "viewer");
|
||||
if (!ctx) return null;
|
||||
let docs: AssetDoc[];
|
||||
if (assetId) {
|
||||
const a = await loadAsset(ctx, assetId);
|
||||
if (!a) return null;
|
||||
docs = [a];
|
||||
} else {
|
||||
docs = (
|
||||
await AssetModel.find({ householdId: ctx.m.householdId, deletedAt: null }).lean<AssetDoc[]>()
|
||||
).filter((a) => visibleAsset(ctx, a._id));
|
||||
}
|
||||
const movements = await loadMovements(
|
||||
ctx.m.householdId,
|
||||
docs.map((a) => a._id),
|
||||
);
|
||||
const to = todayIstanbul();
|
||||
let from: string;
|
||||
if (range === "tum") {
|
||||
const first = [...movements.values()]
|
||||
.flat()
|
||||
.map((m) => m.date)
|
||||
.sort()[0];
|
||||
from = first && first < to ? first : addDays(to, -1);
|
||||
} else from = addMonths(to, -Number(range));
|
||||
const price = await buyPriceLookup(
|
||||
ctx.m.householdId,
|
||||
[...new Set(docs.map((a) => a.assetKey))],
|
||||
to,
|
||||
);
|
||||
const reports = docs.map((a) =>
|
||||
periodReport(movements.get(String(a._id)) ?? [], (date) => price(a.assetKey, date), from, to),
|
||||
);
|
||||
if (assetId) return { ...reports[0]!, excluded: 0 };
|
||||
// Toplamda fiyatı eksik varlıklar (kâr hesaplanamayan) dışarıda bırakılır ve sayılır.
|
||||
const priced = reports.filter((r) => r.profitMinor !== null);
|
||||
return { ...sumReports(priced, from, to), excluded: reports.length - priced.length };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user