Rework real estate model: purchase price + mortgage + amortization only, no appreciation; amortization counts against savings quota; sale price/tax entered at transition time with correct mortgage payoff; carry over income/expenses to new phases
Deploy App / deploy (push) Successful in 1m31s
Deploy App / deploy (push) Successful in 1m31s
This commit is contained in:
@@ -0,0 +1,10 @@
|
|||||||
|
-- AlterTable: RealEstate vereinfacht (Kaufpreis statt Marktwert, keine Wertsteigerung
|
||||||
|
-- mehr, Verkaufspreis/-steuer wandern in PhaseTransitionItem)
|
||||||
|
ALTER TABLE "RealEstate" RENAME COLUMN "marketValue" TO "purchasePrice";
|
||||||
|
ALTER TABLE "RealEstate" DROP COLUMN "valueGrowth";
|
||||||
|
ALTER TABLE "RealEstate" DROP COLUMN "salePrice";
|
||||||
|
ALTER TABLE "RealEstate" DROP COLUMN "saleTaxRate";
|
||||||
|
ALTER TABLE "RealEstate" ADD COLUMN "carriedFromRealEstateId" TEXT;
|
||||||
|
|
||||||
|
-- AlterTable: Verkaufssteuer wird beim Immobilien-Verkauf im Uebergang selbst erfasst
|
||||||
|
ALTER TABLE "PhaseTransitionItem" ADD COLUMN "saleTaxRate" DOUBLE PRECISION;
|
||||||
+10
-6
@@ -176,19 +176,21 @@ model Security {
|
|||||||
transitionItems PhaseTransitionItem[]
|
transitionItems PhaseTransitionItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Eine Immobilie innerhalb einer Phase
|
// Eine Immobilie innerhalb einer Phase. Vereinfachtes Modell: kein Wertsteigerungsfeld,
|
||||||
|
// der Kaufpreis bleibt ueber die Haltedauer fix -- einzig die Hypothek sinkt durch
|
||||||
|
// Amortisation. Verkaufspreis/-steuer werden nicht hier, sondern erst im Uebergangs-
|
||||||
|
// Screen im Moment des Verkaufs erfasst (siehe PhaseTransitionItem).
|
||||||
model RealEstate {
|
model RealEstate {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
phaseId String
|
phaseId String
|
||||||
phase Phase @relation(fields: [phaseId], references: [id], onDelete: Cascade)
|
phase Phase @relation(fields: [phaseId], references: [id], onDelete: Cascade)
|
||||||
name String
|
name String
|
||||||
marketValue Float
|
purchasePrice Float
|
||||||
mortgage Float
|
mortgage Float
|
||||||
valueGrowth Float
|
|
||||||
amortization Float
|
amortization Float
|
||||||
salePrice Float?
|
// Verweist auf die Immobilie der Vorphase, aus der automatisch uebernommen wurde
|
||||||
// Geschaetzte Grundstueckgewinnsteuer (%), direkt am ausloesenden Ereignis erfasst (Kap. 9)
|
// (nur intern zur Deduplizierung bei wiederholtem Speichern des Uebergangs, kein FK).
|
||||||
saleTaxRate Float @default(20)
|
carriedFromRealEstateId String?
|
||||||
|
|
||||||
transitionItems PhaseTransitionItem[]
|
transitionItems PhaseTransitionItem[]
|
||||||
}
|
}
|
||||||
@@ -242,4 +244,6 @@ model PhaseTransitionItem {
|
|||||||
realEstate RealEstate? @relation(fields: [realEstateId], references: [id], onDelete: Cascade)
|
realEstate RealEstate? @relation(fields: [realEstateId], references: [id], onDelete: Cascade)
|
||||||
decision TransitionDecision
|
decision TransitionDecision
|
||||||
salePrice Float?
|
salePrice Float?
|
||||||
|
// Nur bei Immobilien-Verkauf erfasst (Grundstueckgewinnsteuer in %), siehe RealEstate.
|
||||||
|
saleTaxRate Float?
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,12 +23,10 @@ const securitySchema = z.object({
|
|||||||
});
|
});
|
||||||
const realEstateSchema = z.object({
|
const realEstateSchema = z.object({
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
marketValue: z.number(),
|
// Muss zwingend angegeben werden (siehe Anforderung: Kaufpreis ist Pflichtfeld).
|
||||||
|
purchasePrice: z.number().positive("Kaufpreis muss groesser als 0 sein."),
|
||||||
mortgage: z.number(),
|
mortgage: z.number(),
|
||||||
valueGrowth: z.number(),
|
|
||||||
amortization: z.number(),
|
amortization: z.number(),
|
||||||
salePrice: z.number().nullable().optional(),
|
|
||||||
saleTaxRate: z.number().min(0).max(100),
|
|
||||||
});
|
});
|
||||||
const oneTimeEventSchema = z.object({
|
const oneTimeEventSchema = z.object({
|
||||||
type: z.enum(["INCOME", "EXPENSE"]),
|
type: z.enum(["INCOME", "EXPENSE"]),
|
||||||
@@ -96,7 +94,7 @@ export async function PUT(
|
|||||||
incomeEntries: { create: data.incomeEntries.map((e) => ({ ...e, label: e.label ?? null, personId: e.personId ?? null })) },
|
incomeEntries: { create: data.incomeEntries.map((e) => ({ ...e, label: e.label ?? null, personId: e.personId ?? null })) },
|
||||||
expenseEntries: { create: data.expenseEntries.map((e) => ({ ...e, label: e.label ?? null })) },
|
expenseEntries: { create: data.expenseEntries.map((e) => ({ ...e, label: e.label ?? null })) },
|
||||||
securities: { create: data.securities },
|
securities: { create: data.securities },
|
||||||
realEstates: { create: data.realEstates.map((re) => ({ ...re, salePrice: re.salePrice ?? null })) },
|
realEstates: { create: data.realEstates },
|
||||||
oneTimeEvents: { create: data.oneTimeEvents.map((e) => ({ ...e, description: e.description ?? null })) },
|
oneTimeEvents: { create: data.oneTimeEvents.map((e) => ({ ...e, description: e.description ?? null })) },
|
||||||
retirementInfos: { create: data.retirementInfos },
|
retirementInfos: { create: data.retirementInfos },
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { prisma } from "@/lib/db";
|
import { prisma } from "@/lib/db";
|
||||||
import { computeSecurityYearlyValues } from "@/lib/calculations";
|
import { computeMortgageYearly, computeSecurityYearlyValues } from "@/lib/calculations";
|
||||||
import { floorToThousand } from "@/lib/format";
|
import { floorToThousand } from "@/lib/format";
|
||||||
|
|
||||||
const transitionItemSchema = z.object({
|
const transitionItemSchema = z.object({
|
||||||
@@ -10,6 +10,8 @@ const transitionItemSchema = z.object({
|
|||||||
realEstateId: z.string().nullable().optional(),
|
realEstateId: z.string().nullable().optional(),
|
||||||
decision: z.enum(["CARRY_OVER", "SELL"]),
|
decision: z.enum(["CARRY_OVER", "SELL"]),
|
||||||
salePrice: z.number().nullable().optional(),
|
salePrice: z.number().nullable().optional(),
|
||||||
|
// Nur bei Immobilien-Verkauf relevant (Grundstueckgewinnsteuer in %).
|
||||||
|
saleTaxRate: z.number().min(0).max(100).nullable().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const putTransitionSchema = z.object({
|
const putTransitionSchema = z.object({
|
||||||
@@ -51,10 +53,10 @@ export async function GET(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Speichert die Entscheidungen (Uebernehmen/Verkaufen) fuer jede Position der Vorphase.
|
// Speichert die Entscheidungen (Uebernehmen/Verkaufen bzw. Halten/Verkaufen) fuer jede
|
||||||
// Uebernommene Wertschriften werden automatisch als neue Wertschrift in der Folgephase
|
// Position der Vorphase. Uebernommene/gehaltene Positionen werden automatisch 1:1 (mit
|
||||||
// angelegt (Startwert = Endwert dieser Phase). Verkaufte Positionen fliessen als
|
// zurueckgesetztem Sparbeitrag/Amortisation) in der Folgephase angelegt. Verkaufte
|
||||||
// "verfuegbares Startkapital" (Phase.incomingCapital) in die Folgephase ein.
|
// Positionen fliessen als "verfuegbares Startkapital" (Phase.incomingCapital) ein.
|
||||||
export async function PUT(
|
export async function PUT(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
{ params }: { params: Promise<{ phaseId: string }> }
|
{ params }: { params: Promise<{ phaseId: string }> }
|
||||||
@@ -94,7 +96,7 @@ export async function PUT(
|
|||||||
const missing = [...requiredIds].filter((id) => !providedIds.has(id));
|
const missing = [...requiredIds].filter((id) => !providedIds.has(id));
|
||||||
if (missing.length > 0) {
|
if (missing.length > 0) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Fuer jede bestehende Position muss Uebernehmen oder Verkaufen gewaehlt werden." },
|
{ error: "Fuer jede bestehende Position muss Uebernehmen/Halten oder Verkaufen gewaehlt werden." },
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -104,6 +106,7 @@ export async function PUT(
|
|||||||
|
|
||||||
let incomingCapital = 0;
|
let incomingCapital = 0;
|
||||||
const securitiesToCarry: { source: (typeof phase.securities)[number]; endValue: number }[] = [];
|
const securitiesToCarry: { source: (typeof phase.securities)[number]; endValue: number }[] = [];
|
||||||
|
const realEstatesToCarry: { source: (typeof phase.realEstates)[number]; remainingMortgage: number }[] = [];
|
||||||
|
|
||||||
for (const item of parsed.data.items) {
|
for (const item of parsed.data.items) {
|
||||||
if (item.positionType === "SECURITY" && item.securityId) {
|
if (item.positionType === "SECURITY" && item.securityId) {
|
||||||
@@ -125,11 +128,22 @@ export async function PUT(
|
|||||||
}
|
}
|
||||||
} else if (item.positionType === "REAL_ESTATE" && item.realEstateId) {
|
} else if (item.positionType === "REAL_ESTATE" && item.realEstateId) {
|
||||||
const realEstate = realEstateById.get(item.realEstateId);
|
const realEstate = realEstateById.get(item.realEstateId);
|
||||||
if (!realEstate || item.decision !== "SELL") continue;
|
if (!realEstate) continue;
|
||||||
|
const remainingMortgage = computeMortgageYearly(
|
||||||
|
realEstate.mortgage,
|
||||||
|
realEstate.amortization,
|
||||||
|
phase.durationYears
|
||||||
|
)[phase.durationYears];
|
||||||
|
|
||||||
|
if (item.decision === "CARRY_OVER") {
|
||||||
|
realEstatesToCarry.push({ source: realEstate, remainingMortgage });
|
||||||
|
} else {
|
||||||
const salePrice = item.salePrice ?? 0;
|
const salePrice = item.salePrice ?? 0;
|
||||||
const gain = Math.max(0, salePrice - realEstate.marketValue);
|
const saleTaxRate = item.saleTaxRate ?? 0;
|
||||||
const tax = gain * (realEstate.saleTaxRate / 100);
|
const gain = Math.max(0, salePrice - realEstate.purchasePrice);
|
||||||
incomingCapital += salePrice - tax;
|
const tax = gain * (saleTaxRate / 100);
|
||||||
|
incomingCapital += salePrice - remainingMortgage - tax;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,15 +155,21 @@ export async function PUT(
|
|||||||
const transition = await prisma.$transaction(async (tx) => {
|
const transition = await prisma.$transaction(async (tx) => {
|
||||||
await tx.phaseTransition.deleteMany({ where: { fromPhaseId: phaseId } });
|
await tx.phaseTransition.deleteMany({ where: { fromPhaseId: phaseId } });
|
||||||
|
|
||||||
// Vorherige automatisch uebernommene Wertschriften aus einem frueheren Speichern
|
// Vorherige automatisch uebernommene Positionen aus einem frueheren Speichern
|
||||||
// dieses Uebergangs entfernen, damit sie nicht dupliziert werden. Manuell vom
|
// dieses Uebergangs entfernen, damit sie nicht dupliziert werden. Manuell vom
|
||||||
// Benutzer angelegte Wertschriften (carriedFromSecurityId = null) bleiben unberuehrt.
|
// Benutzer angelegte Positionen (carriedFrom...Id = null) bleiben unberuehrt.
|
||||||
await tx.security.deleteMany({
|
await tx.security.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
phaseId: nextPhase.id,
|
phaseId: nextPhase.id,
|
||||||
carriedFromSecurityId: { in: phase.securities.map((s) => s.id) },
|
carriedFromSecurityId: { in: phase.securities.map((s) => s.id) },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
await tx.realEstate.deleteMany({
|
||||||
|
where: {
|
||||||
|
phaseId: nextPhase.id,
|
||||||
|
carriedFromRealEstateId: { in: phase.realEstates.map((re) => re.id) },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
for (const { source, endValue } of securitiesToCarry) {
|
for (const { source, endValue } of securitiesToCarry) {
|
||||||
await tx.security.create({
|
await tx.security.create({
|
||||||
@@ -167,6 +187,19 @@ export async function PUT(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const { source, remainingMortgage } of realEstatesToCarry) {
|
||||||
|
await tx.realEstate.create({
|
||||||
|
data: {
|
||||||
|
phaseId: nextPhase.id,
|
||||||
|
name: source.name,
|
||||||
|
purchasePrice: source.purchasePrice,
|
||||||
|
mortgage: remainingMortgage,
|
||||||
|
amortization: 0,
|
||||||
|
carriedFromRealEstateId: source.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await tx.phase.update({
|
await tx.phase.update({
|
||||||
where: { id: nextPhase.id },
|
where: { id: nextPhase.id },
|
||||||
data: { incomingCapital },
|
data: { incomingCapital },
|
||||||
@@ -183,6 +216,7 @@ export async function PUT(
|
|||||||
realEstateId: i.realEstateId ?? null,
|
realEstateId: i.realEstateId ?? null,
|
||||||
decision: i.decision,
|
decision: i.decision,
|
||||||
salePrice: i.salePrice ?? null,
|
salePrice: i.salePrice ?? null,
|
||||||
|
saleTaxRate: i.saleTaxRate ?? null,
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -31,9 +31,12 @@ export async function POST(
|
|||||||
const lastPhase = await prisma.phase.findFirst({
|
const lastPhase = await prisma.phase.findFirst({
|
||||||
where: { planId },
|
where: { planId },
|
||||||
orderBy: { sequenceNumber: "desc" },
|
orderBy: { sequenceNumber: "desc" },
|
||||||
|
include: { incomeEntries: true, expenseEntries: true },
|
||||||
});
|
});
|
||||||
const nextSequence = (lastPhase?.sequenceNumber ?? 0) + 1;
|
const nextSequence = (lastPhase?.sequenceNumber ?? 0) + 1;
|
||||||
|
|
||||||
|
// Einkommen und Ausgaben werden 1:1 aus der letzten Phase uebernommen (manuell
|
||||||
|
// anpassbar), damit man sie nicht bei jeder neuen Phase erneut eintippen muss.
|
||||||
const phase = await prisma.phase.create({
|
const phase = await prisma.phase.create({
|
||||||
data: {
|
data: {
|
||||||
planId,
|
planId,
|
||||||
@@ -41,7 +44,21 @@ export async function POST(
|
|||||||
name: parsed.data.name,
|
name: parsed.data.name,
|
||||||
durationYears: parsed.data.durationYears,
|
durationYears: parsed.data.durationYears,
|
||||||
inflationRate: parsed.data.inflationRate ?? null,
|
inflationRate: parsed.data.inflationRate ?? null,
|
||||||
incomeMode: parsed.data.incomeMode,
|
incomeMode: lastPhase?.incomeMode ?? parsed.data.incomeMode,
|
||||||
|
incomeEntries: lastPhase
|
||||||
|
? {
|
||||||
|
create: lastPhase.incomeEntries.map((e) => ({
|
||||||
|
personId: e.personId,
|
||||||
|
label: e.label,
|
||||||
|
amount: e.amount,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
expenseEntries: lastPhase
|
||||||
|
? {
|
||||||
|
create: lastPhase.expenseEntries.map((e) => ({ label: e.label, amount: e.amount })),
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
},
|
},
|
||||||
include: phaseInclude,
|
include: phaseInclude,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -80,12 +80,9 @@ export async function POST(
|
|||||||
realEstates: {
|
realEstates: {
|
||||||
create: phase.realEstates.map((re) => ({
|
create: phase.realEstates.map((re) => ({
|
||||||
name: re.name,
|
name: re.name,
|
||||||
marketValue: re.marketValue,
|
purchasePrice: re.purchasePrice,
|
||||||
mortgage: re.mortgage,
|
mortgage: re.mortgage,
|
||||||
valueGrowth: re.valueGrowth,
|
|
||||||
amortization: re.amortization,
|
amortization: re.amortization,
|
||||||
salePrice: re.salePrice,
|
|
||||||
saleTaxRate: re.saleTaxRate,
|
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
oneTimeEvents: {
|
oneTimeEvents: {
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export function Dashboard({
|
|||||||
computed.phases.map((phase) => {
|
computed.phases.map((phase) => {
|
||||||
const row: Record<string, number | string> = { phase: phase.name };
|
const row: Record<string, number | string> = { phase: phase.name };
|
||||||
for (const s of phase.securities) row[s.name] = s.endValue;
|
for (const s of phase.securities) row[s.name] = s.endValue;
|
||||||
for (const re of phase.realEstates) row[re.name] = re.endContribution;
|
for (const re of phase.realEstates) row[re.name] = re.endNet;
|
||||||
return row;
|
return row;
|
||||||
}),
|
}),
|
||||||
[computed]
|
[computed]
|
||||||
|
|||||||
@@ -57,7 +57,11 @@ export function PhaseForm({ household, phase, onSaved, onCancel }: Props) {
|
|||||||
const totalIncome = incomeEntries.reduce((s, e) => s + e.amount, 0);
|
const totalIncome = incomeEntries.reduce((s, e) => s + e.amount, 0);
|
||||||
const totalExpense = expenseEntries.reduce((s, e) => s + e.amount, 0);
|
const totalExpense = expenseEntries.reduce((s, e) => s + e.amount, 0);
|
||||||
const savingsQuota = totalIncome - totalExpense;
|
const savingsQuota = totalIncome - totalExpense;
|
||||||
const allocated = securities.reduce((s, sec) => s + sec.annualContribution, 0);
|
// Amortisationsraten aller Immobilien zaehlen gleichwertig mit den Sparbeitraegen der
|
||||||
|
// Wertschriften gegen dieselbe verfuegbare Sparquote.
|
||||||
|
const allocated =
|
||||||
|
securities.reduce((s, sec) => s + sec.annualContribution, 0) +
|
||||||
|
realEstates.reduce((s, re) => s + re.amortization, 0);
|
||||||
const overAllocated = allocated > savingsQuota;
|
const overAllocated = allocated > savingsQuota;
|
||||||
const savingsRemaining = savingsQuota - allocated > 0.5;
|
const savingsRemaining = savingsQuota - allocated > 0.5;
|
||||||
|
|
||||||
@@ -68,6 +72,13 @@ export function PhaseForm({ household, phase, onSaved, onCancel }: Props) {
|
|||||||
const startCapitalRemaining = phase.incomingCapital - allocatedStartCapital > 0.5;
|
const startCapitalRemaining = phase.incomingCapital - allocatedStartCapital > 0.5;
|
||||||
|
|
||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
|
const missingPurchasePrice = realEstates.find((re) => !re.purchasePrice || re.purchasePrice <= 0);
|
||||||
|
if (missingPurchasePrice) {
|
||||||
|
setError(
|
||||||
|
`Bitte fuer "${missingPurchasePrice.name || "Immobilie"}" einen Kaufpreis groesser als 0 eintragen.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
@@ -302,42 +313,23 @@ export function PhaseForm({ household, phase, onSaved, onCancel }: Props) {
|
|||||||
onChange={(v) => setRealEstates((prev) => prev.map((x, idx) => (idx === i ? { ...x, name: v } : x)))}
|
onChange={(v) => setRealEstates((prev) => prev.map((x, idx) => (idx === i ? { ...x, name: v } : x)))}
|
||||||
/>
|
/>
|
||||||
<MoneyField
|
<MoneyField
|
||||||
label="Aktueller Marktwert (CHF)"
|
label="Kaufpreis (CHF)"
|
||||||
help="Geschaetzter heutiger Verkehrswert der Liegenschaft."
|
help="Pflichtfeld. Der Kaufpreis bleibt ueber die ganze Haltedauer fix -- es wird keine Wertsteigerung angenommen, nur die Hypothek sinkt durch Amortisation."
|
||||||
value={re.marketValue}
|
value={re.purchasePrice}
|
||||||
onChange={(v) => setRealEstates((prev) => prev.map((x, idx) => (idx === i ? { ...x, marketValue: v } : x)))}
|
onChange={(v) => setRealEstates((prev) => prev.map((x, idx) => (idx === i ? { ...x, purchasePrice: v } : x)))}
|
||||||
/>
|
/>
|
||||||
<MoneyField
|
<MoneyField
|
||||||
label="Aktuelle Hypothek (CHF)"
|
label="Hypothek (CHF)"
|
||||||
help="Ausstehender Hypothekarbetrag."
|
help="Ausstehender Hypothekarbetrag zu Beginn der Phase."
|
||||||
value={re.mortgage}
|
value={re.mortgage}
|
||||||
onChange={(v) => setRealEstates((prev) => prev.map((x, idx) => (idx === i ? { ...x, mortgage: v } : x)))}
|
onChange={(v) => setRealEstates((prev) => prev.map((x, idx) => (idx === i ? { ...x, mortgage: v } : x)))}
|
||||||
/>
|
/>
|
||||||
<NumberField
|
|
||||||
label="Wertsteigerung (%/Jahr)"
|
|
||||||
help="Ihre Annahme zur Wertentwicklung der Immobilie pro Jahr."
|
|
||||||
step={0.1}
|
|
||||||
value={re.valueGrowth}
|
|
||||||
onChange={(v) => setRealEstates((prev) => prev.map((x, idx) => (idx === i ? { ...x, valueGrowth: v } : x)))}
|
|
||||||
/>
|
|
||||||
<MoneyField
|
<MoneyField
|
||||||
label="Jaehrliche Amortisation (CHF)"
|
label="Amortisationsrate (CHF/Jahr)"
|
||||||
help="Betrag, um den die Hypothek pro Jahr reduziert wird."
|
help="Betrag, um den die Hypothek pro Jahr reduziert wird. Zaehlt zusammen mit den Sparbeitraegen der Wertschriften gegen die verfuegbare Sparquote."
|
||||||
value={re.amortization}
|
value={re.amortization}
|
||||||
onChange={(v) => setRealEstates((prev) => prev.map((x, idx) => (idx === i ? { ...x, amortization: v } : x)))}
|
onChange={(v) => setRealEstates((prev) => prev.map((x, idx) => (idx === i ? { ...x, amortization: v } : x)))}
|
||||||
/>
|
/>
|
||||||
<MoneyField
|
|
||||||
label="Geschaetzter Verkaufspreis (CHF)"
|
|
||||||
help="Nur bei geplantem Verkauf am Ende der Phase auszufuellen."
|
|
||||||
value={re.salePrice ?? 0}
|
|
||||||
onChange={(v) => setRealEstates((prev) => prev.map((x, idx) => (idx === i ? { ...x, salePrice: v || null } : x)))}
|
|
||||||
/>
|
|
||||||
<NumberField
|
|
||||||
label="Geschaetzte Grundstueckgewinnsteuer (%)"
|
|
||||||
help="Kantonale Steuer auf den Verkaufsgewinn, ca. 10-30% je nach Kanton und Besitzdauer."
|
|
||||||
value={re.saleTaxRate}
|
|
||||||
onChange={(v) => setRealEstates((prev) => prev.map((x, idx) => (idx === i ? { ...x, saleTaxRate: v } : x)))}
|
|
||||||
/>
|
|
||||||
<div className="flex items-end">
|
<div className="flex items-end">
|
||||||
<RemoveButton onClick={() => setRealEstates((prev) => prev.filter((_, idx) => idx !== i))} />
|
<RemoveButton onClick={() => setRealEstates((prev) => prev.filter((_, idx) => idx !== i))} />
|
||||||
</div>
|
</div>
|
||||||
@@ -348,7 +340,7 @@ export function PhaseForm({ household, phase, onSaved, onCancel }: Props) {
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
setRealEstates((prev) => [
|
setRealEstates((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
{ id: tempId(), name: "", marketValue: 0, mortgage: 0, valueGrowth: 0, amortization: 0, salePrice: null, saleTaxRate: 20 },
|
{ id: tempId(), name: "", purchasePrice: 0, mortgage: 0, amortization: 0 },
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -13,10 +13,13 @@ interface ItemDraft {
|
|||||||
name: string;
|
name: string;
|
||||||
decision: TransitionDecision;
|
decision: TransitionDecision;
|
||||||
salePrice: number | null;
|
salePrice: number | null;
|
||||||
// Referenzwerte fuer die Anzeige/Berechnung des verfuegbaren Startkapitals
|
// Nur fuer Immobilien editierbar (poppt bei "Verkaufen" auf); bei Wertschriften der
|
||||||
carryOverValue: number;
|
// fixe, am Wertpapier hinterlegte Steuersatz.
|
||||||
originalValue: number;
|
|
||||||
saleTaxRate: number;
|
saleTaxRate: number;
|
||||||
|
// Referenzwerte fuer die Anzeige/Berechnung des verfuegbaren Startkapitals
|
||||||
|
carryOverValue: number; // Wert bei "Halten": Endwert (Wertschrift) bzw. Nettowert (Immobilie)
|
||||||
|
originalValue: number; // Wertschrift: Startwert: Immobilie: Kaufpreis
|
||||||
|
remainingMortgage: number; // nur Immobilien: Resthypothek am Ende der Phase
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TransitionPanel({
|
export function TransitionPanel({
|
||||||
@@ -48,30 +51,42 @@ export function TransitionPanel({
|
|||||||
name: s.name,
|
name: s.name,
|
||||||
decision: "CARRY_OVER" as TransitionDecision,
|
decision: "CARRY_OVER" as TransitionDecision,
|
||||||
salePrice: null,
|
salePrice: null,
|
||||||
|
saleTaxRate: s.saleTaxRate,
|
||||||
carryOverValue: c?.endValue ?? 0,
|
carryOverValue: c?.endValue ?? 0,
|
||||||
originalValue: c?.startValue ?? 0,
|
originalValue: c?.startValue ?? 0,
|
||||||
saleTaxRate: s.saleTaxRate,
|
remainingMortgage: 0,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
...phase.realEstates.map((re) => {
|
...phase.realEstates.map((re) => {
|
||||||
const c = computed.realEstates.find((cr) => cr.id === re.id);
|
const c = computed.realEstates.find((cr) => cr.id === re.id);
|
||||||
|
const remainingMortgage = c ? c.mortgages[phase.durationYears] : 0;
|
||||||
return {
|
return {
|
||||||
positionType: "REAL_ESTATE" as const,
|
positionType: "REAL_ESTATE" as const,
|
||||||
id: re.id,
|
id: re.id,
|
||||||
name: re.name,
|
name: re.name,
|
||||||
decision: "CARRY_OVER" as TransitionDecision,
|
decision: "CARRY_OVER" as TransitionDecision,
|
||||||
salePrice: re.marketValue,
|
salePrice: re.purchasePrice,
|
||||||
carryOverValue: c?.endNetIfKept ?? 0,
|
saleTaxRate: 20,
|
||||||
originalValue: re.marketValue,
|
carryOverValue: c?.endNet ?? 0,
|
||||||
saleTaxRate: re.saleTaxRate,
|
originalValue: re.purchasePrice,
|
||||||
|
remainingMortgage,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await api.get<{ transition: { items: { positionType: string; securityId: string | null; realEstateId: string | null; decision: TransitionDecision; salePrice: number | null }[] } | null }>(
|
const data = await api.get<{
|
||||||
`/api/phases/${phase.id}/transition`
|
transition: {
|
||||||
);
|
items: {
|
||||||
|
positionType: string;
|
||||||
|
securityId: string | null;
|
||||||
|
realEstateId: string | null;
|
||||||
|
decision: TransitionDecision;
|
||||||
|
salePrice: number | null;
|
||||||
|
saleTaxRate: number | null;
|
||||||
|
}[];
|
||||||
|
} | null;
|
||||||
|
}>(`/api/phases/${phase.id}/transition`);
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
if (data.transition) {
|
if (data.transition) {
|
||||||
for (const savedItem of data.transition.items) {
|
for (const savedItem of data.transition.items) {
|
||||||
@@ -81,6 +96,7 @@ export function TransitionPanel({
|
|||||||
if (target) {
|
if (target) {
|
||||||
target.decision = savedItem.decision;
|
target.decision = savedItem.decision;
|
||||||
if (savedItem.salePrice != null) target.salePrice = savedItem.salePrice;
|
if (savedItem.salePrice != null) target.salePrice = savedItem.salePrice;
|
||||||
|
if (savedItem.saleTaxRate != null) target.saleTaxRate = savedItem.saleTaxRate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,17 +124,16 @@ export function TransitionPanel({
|
|||||||
|
|
||||||
const totalAvailableCapital = floorToThousand(
|
const totalAvailableCapital = floorToThousand(
|
||||||
items.reduce((sum, it) => {
|
items.reduce((sum, it) => {
|
||||||
if (it.positionType === "SECURITY") {
|
|
||||||
if (it.decision === "CARRY_OVER") return sum;
|
if (it.decision === "CARRY_OVER") return sum;
|
||||||
|
if (it.positionType === "SECURITY") {
|
||||||
const gain = Math.max(0, it.carryOverValue - it.originalValue);
|
const gain = Math.max(0, it.carryOverValue - it.originalValue);
|
||||||
const tax = gain * (it.saleTaxRate / 100);
|
const tax = gain * (it.saleTaxRate / 100);
|
||||||
return sum + (it.carryOverValue - tax);
|
return sum + (it.carryOverValue - tax);
|
||||||
}
|
}
|
||||||
if (it.decision === "CARRY_OVER") return sum;
|
|
||||||
const salePrice = it.salePrice ?? 0;
|
const salePrice = it.salePrice ?? 0;
|
||||||
const gain = Math.max(0, salePrice - it.originalValue);
|
const gain = Math.max(0, salePrice - it.originalValue);
|
||||||
const tax = gain * (it.saleTaxRate / 100);
|
const tax = gain * (it.saleTaxRate / 100);
|
||||||
return sum + (salePrice - tax);
|
return sum + (salePrice - it.remainingMortgage - tax);
|
||||||
}, 0)
|
}, 0)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -134,6 +149,7 @@ export function TransitionPanel({
|
|||||||
realEstateId: it.positionType === "REAL_ESTATE" ? it.id : null,
|
realEstateId: it.positionType === "REAL_ESTATE" ? it.id : null,
|
||||||
decision: it.decision,
|
decision: it.decision,
|
||||||
salePrice: it.positionType === "REAL_ESTATE" && it.decision === "SELL" ? it.salePrice : null,
|
salePrice: it.positionType === "REAL_ESTATE" && it.decision === "SELL" ? it.salePrice : null,
|
||||||
|
saleTaxRate: it.positionType === "REAL_ESTATE" && it.decision === "SELL" ? it.saleTaxRate : null,
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
setSaved(true);
|
setSaved(true);
|
||||||
@@ -159,7 +175,8 @@ export function TransitionPanel({
|
|||||||
<tr className="text-left text-xs text-zinc-500">
|
<tr className="text-left text-xs text-zinc-500">
|
||||||
<th className="pb-1 font-normal">Position</th>
|
<th className="pb-1 font-normal">Position</th>
|
||||||
<th className="pb-1 font-normal">Entscheidung</th>
|
<th className="pb-1 font-normal">Entscheidung</th>
|
||||||
<th className="pb-1 font-normal">Verkaufspreis / Wert</th>
|
<th className="pb-1 font-normal">Verkaufspreis</th>
|
||||||
|
<th className="pb-1 font-normal">Grundstueckgewinnsteuer</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -176,12 +193,13 @@ export function TransitionPanel({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<option value="CARRY_OVER">Uebernehmen</option>
|
<option value="CARRY_OVER">{it.positionType === "REAL_ESTATE" ? "Halten" : "Uebernehmen"}</option>
|
||||||
<option value="SELL">Verkaufen</option>
|
<option value="SELL">Verkaufen</option>
|
||||||
</select>
|
</select>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-2">
|
<td className="py-2 pr-2">
|
||||||
{it.positionType === "REAL_ESTATE" && it.decision === "SELL" ? (
|
{it.decision === "SELL" ? (
|
||||||
|
it.positionType === "REAL_ESTATE" ? (
|
||||||
<MoneyInput
|
<MoneyInput
|
||||||
className="w-32"
|
className="w-32"
|
||||||
value={it.salePrice ?? 0}
|
value={it.salePrice ?? 0}
|
||||||
@@ -191,6 +209,25 @@ export function TransitionPanel({
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-xs text-zinc-500">{formatChf(it.carryOverValue)} CHF</span>
|
<span className="text-xs text-zinc-500">{formatChf(it.carryOverValue)} CHF</span>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<span className="text-xs text-zinc-500">{formatChf(it.carryOverValue)} CHF</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="py-2">
|
||||||
|
{it.positionType === "REAL_ESTATE" && it.decision === "SELL" ? (
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="w-20 rounded-md border border-zinc-300 bg-white px-2 py-1 text-xs dark:border-zinc-600 dark:bg-zinc-900"
|
||||||
|
value={it.saleTaxRate}
|
||||||
|
onChange={(e) =>
|
||||||
|
setItems((prev) =>
|
||||||
|
prev.map((x, idx) => (idx === i ? { ...x, saleTaxRate: e.target.valueAsNumber || 0 } : x))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span className="text-xs text-zinc-500">—</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -201,7 +238,8 @@ export function TransitionPanel({
|
|||||||
Verfuegbares Startkapital fuer neue Phase (aus Verkaeufen): <strong>{formatChf(totalAvailableCapital)} CHF</strong>
|
Verfuegbares Startkapital fuer neue Phase (aus Verkaeufen): <strong>{formatChf(totalAvailableCapital)} CHF</strong>
|
||||||
<p className="mt-1 text-xs text-zinc-500">
|
<p className="mt-1 text-xs text-zinc-500">
|
||||||
Wird beim Speichern automatisch in "{nextPhaseName}" als verfuegbares Startkapital hinterlegt.
|
Wird beim Speichern automatisch in "{nextPhaseName}" als verfuegbares Startkapital hinterlegt.
|
||||||
Uebernommene Wertschriften erscheinen dort automatisch mit ihrem Endwert als neuer Startwert.
|
Gehaltene/uebernommene Positionen erscheinen dort automatisch mit ihrem Endwert (Wertschriften) bzw.
|
||||||
|
Kaufpreis/Resthypothek (Immobilien) als neue Ausgangswerte.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{error && <p className="text-sm text-red-600 dark:text-red-400">{error}</p>}
|
{error && <p className="text-sm text-red-600 dark:text-red-400">{error}</p>}
|
||||||
|
|||||||
+24
-49
@@ -14,14 +14,10 @@ export interface SecurityComputed {
|
|||||||
export interface RealEstateComputed {
|
export interface RealEstateComputed {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
purchasePrice: number; // fix ueber die Haltedauer, keine Wertsteigerung im vereinfachten Modell
|
||||||
startNet: number;
|
startNet: number;
|
||||||
endNetIfKept: number;
|
endNet: number;
|
||||||
sold: boolean;
|
mortgages: number[]; // Index 0 = Start, Index durationYears = Ende
|
||||||
saleNetProceeds: number | null;
|
|
||||||
taxAmount: number;
|
|
||||||
endContribution: number; // was tatsaechlich in die Endvermoegens-Summe der Phase einfliesst
|
|
||||||
marketValues: number[];
|
|
||||||
mortgages: number[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RetirementComputed {
|
export interface RetirementComputed {
|
||||||
@@ -88,20 +84,18 @@ export function computeSecurityYearlyValues(
|
|||||||
return values;
|
return values;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function computeRealEstateYearly(
|
// Vereinfachtes Modell (keine Wertsteigerung): der Kaufpreis bleibt ueber die ganze
|
||||||
marketValue: number,
|
// Haltedauer fix, nur die Hypothek sinkt jaehrlich um die Amortisationsrate.
|
||||||
|
export function computeMortgageYearly(
|
||||||
mortgage: number,
|
mortgage: number,
|
||||||
valueGrowth: number,
|
|
||||||
amortization: number,
|
amortization: number,
|
||||||
durationYears: number
|
durationYears: number
|
||||||
): { marketValues: number[]; mortgages: number[] } {
|
): number[] {
|
||||||
const marketValues = [floorToThousand(marketValue)];
|
|
||||||
const mortgages = [floorToThousand(mortgage)];
|
const mortgages = [floorToThousand(mortgage)];
|
||||||
for (let year = 1; year <= durationYears; year++) {
|
for (let year = 1; year <= durationYears; year++) {
|
||||||
marketValues.push(floorToThousand(marketValues[year - 1] * (1 + valueGrowth / 100)));
|
|
||||||
mortgages.push(floorToThousand(Math.max(0, mortgages[year - 1] - amortization)));
|
mortgages.push(floorToThousand(Math.max(0, mortgages[year - 1] - amortization)));
|
||||||
}
|
}
|
||||||
return { marketValues, mortgages };
|
return mortgages;
|
||||||
}
|
}
|
||||||
|
|
||||||
function computeRetirement(
|
function computeRetirement(
|
||||||
@@ -149,7 +143,11 @@ function computePhase(
|
|||||||
const retirement = computeRetirement(household, phase);
|
const retirement = computeRetirement(household, phase);
|
||||||
const effectiveIncome = incomeFromEntries + (retirement?.totalPensionIncome ?? 0);
|
const effectiveIncome = incomeFromEntries + (retirement?.totalPensionIncome ?? 0);
|
||||||
const savingsQuota = effectiveIncome - expenseTotal;
|
const savingsQuota = effectiveIncome - expenseTotal;
|
||||||
const allocatedSavings = phase.securities.reduce((sum, s) => sum + s.annualContribution, 0);
|
// Amortisationsraten aller Immobilien zaehlen gleichwertig mit den Sparbeitraegen der
|
||||||
|
// Wertschriften gegen dieselbe verfuegbare Sparquote (ein gemeinsamer Topf).
|
||||||
|
const allocatedSavings =
|
||||||
|
phase.securities.reduce((sum, s) => sum + s.annualContribution, 0) +
|
||||||
|
phase.realEstates.reduce((sum, re) => sum + re.amortization, 0);
|
||||||
const savingsWarning = allocatedSavings > savingsQuota;
|
const savingsWarning = allocatedSavings > savingsQuota;
|
||||||
|
|
||||||
const securities: SecurityComputed[] = phase.securities.map((s) => {
|
const securities: SecurityComputed[] = phase.securities.map((s) => {
|
||||||
@@ -170,34 +168,14 @@ function computePhase(
|
|||||||
});
|
});
|
||||||
|
|
||||||
const realEstates: RealEstateComputed[] = phase.realEstates.map((re) => {
|
const realEstates: RealEstateComputed[] = phase.realEstates.map((re) => {
|
||||||
const { marketValues, mortgages } = computeRealEstateYearly(
|
const mortgages = computeMortgageYearly(re.mortgage, re.amortization, phase.durationYears);
|
||||||
re.marketValue,
|
const purchasePrice = floorToThousand(re.purchasePrice);
|
||||||
re.mortgage,
|
|
||||||
re.valueGrowth,
|
|
||||||
re.amortization,
|
|
||||||
phase.durationYears
|
|
||||||
);
|
|
||||||
const startNet = marketValues[0] - mortgages[0];
|
|
||||||
const endNetIfKept = marketValues[phase.durationYears] - mortgages[phase.durationYears];
|
|
||||||
const sold = re.salePrice != null;
|
|
||||||
let saleNetProceeds: number | null = null;
|
|
||||||
let taxAmount = 0;
|
|
||||||
if (sold) {
|
|
||||||
// Vereinfachung gemaess TDD 3.3: Gewinn = Verkaufspreis - urspruenglich erfasster Startwert
|
|
||||||
const gain = Math.max(0, re.salePrice! - re.marketValue);
|
|
||||||
taxAmount = gain * (re.saleTaxRate / 100);
|
|
||||||
saleNetProceeds = floorToThousand(re.salePrice! - taxAmount);
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
id: re.id,
|
id: re.id,
|
||||||
name: re.name,
|
name: re.name,
|
||||||
startNet,
|
purchasePrice,
|
||||||
endNetIfKept,
|
startNet: purchasePrice - mortgages[0],
|
||||||
sold,
|
endNet: purchasePrice - mortgages[phase.durationYears],
|
||||||
saleNetProceeds,
|
|
||||||
taxAmount,
|
|
||||||
endContribution: sold ? saleNetProceeds! : endNetIfKept,
|
|
||||||
marketValues,
|
|
||||||
mortgages,
|
mortgages,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -213,7 +191,7 @@ function computePhase(
|
|||||||
|
|
||||||
const endWealthNominal =
|
const endWealthNominal =
|
||||||
securities.reduce((sum, s) => sum + s.endValue, 0) +
|
securities.reduce((sum, s) => sum + s.endValue, 0) +
|
||||||
realEstates.reduce((sum, re) => sum + re.endContribution, 0) +
|
realEstates.reduce((sum, re) => sum + re.endNet, 0) +
|
||||||
oneTimeNet +
|
oneTimeNet +
|
||||||
(retirement?.lumpSumNetTotal ?? 0);
|
(retirement?.lumpSumNetTotal ?? 0);
|
||||||
|
|
||||||
@@ -226,16 +204,13 @@ function computePhase(
|
|||||||
for (let year = 1; year <= phase.durationYears; year++) {
|
for (let year = 1; year <= phase.durationYears; year++) {
|
||||||
let value =
|
let value =
|
||||||
securities.reduce((sum, s) => sum + s.yearly[year], 0) +
|
securities.reduce((sum, s) => sum + s.yearly[year], 0) +
|
||||||
realEstates.reduce((sum, re) => sum + (re.marketValues[year] - re.mortgages[year]), 0);
|
realEstates.reduce((sum, re) => sum + (re.purchasePrice - re.mortgages[year]), 0);
|
||||||
if (year === phase.durationYears) {
|
if (year === phase.durationYears) {
|
||||||
// Einmalige Ereignisse, Verkaufserloese und Kapitalbezuege schlagen erst am Ende
|
// Einmalige Ereignisse und Kapitalbezuege schlagen erst am Ende der Phase zu
|
||||||
// der Phase zu Buche (siehe Phasenuebergang, TDD Kapitel 10).
|
// Buche (siehe Phasenuebergang, TDD Kapitel 10). Immobilien-/Wertschriften-
|
||||||
|
// Verkaeufe wirken sich nur auf die naechste Phase aus (incomingCapital), nicht
|
||||||
|
// mehr auf das Endvermoegen dieser Phase selbst.
|
||||||
value += oneTimeNet + (retirement?.lumpSumNetTotal ?? 0);
|
value += oneTimeNet + (retirement?.lumpSumNetTotal ?? 0);
|
||||||
const soldReplacement = realEstates.reduce(
|
|
||||||
(sum, re) => sum + (re.sold ? re.saleNetProceeds! - (re.marketValues[year] - re.mortgages[year]) : 0),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
value += soldReplacement;
|
|
||||||
}
|
}
|
||||||
yearlyNominal.push(value);
|
yearlyNominal.push(value);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-4
@@ -74,12 +74,9 @@ export function toPlanInput(plan: PlanWithRelations): PlanInput {
|
|||||||
realEstates: phase.realEstates.map((re) => ({
|
realEstates: phase.realEstates.map((re) => ({
|
||||||
id: re.id,
|
id: re.id,
|
||||||
name: re.name,
|
name: re.name,
|
||||||
marketValue: re.marketValue,
|
purchasePrice: re.purchasePrice,
|
||||||
mortgage: re.mortgage,
|
mortgage: re.mortgage,
|
||||||
valueGrowth: re.valueGrowth,
|
|
||||||
amortization: re.amortization,
|
amortization: re.amortization,
|
||||||
salePrice: re.salePrice,
|
|
||||||
saleTaxRate: re.saleTaxRate,
|
|
||||||
})),
|
})),
|
||||||
oneTimeEvents: phase.oneTimeEvents.map((e) => ({
|
oneTimeEvents: phase.oneTimeEvents.map((e) => ({
|
||||||
id: e.id,
|
id: e.id,
|
||||||
|
|||||||
+1
-4
@@ -53,12 +53,9 @@ export interface SecurityInput {
|
|||||||
export interface RealEstateInput {
|
export interface RealEstateInput {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
marketValue: number;
|
purchasePrice: number;
|
||||||
mortgage: number;
|
mortgage: number;
|
||||||
valueGrowth: number;
|
|
||||||
amortization: number;
|
amortization: number;
|
||||||
salePrice: number | null;
|
|
||||||
saleTaxRate: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OneTimeEventInput {
|
export interface OneTimeEventInput {
|
||||||
|
|||||||
Reference in New Issue
Block a user