diff --git a/frontend/__tests__/TaxReport.test.tsx b/frontend/__tests__/TaxReport.test.tsx new file mode 100644 index 00000000..e5381a05 --- /dev/null +++ b/frontend/__tests__/TaxReport.test.tsx @@ -0,0 +1,90 @@ +/** + * __tests__/TaxReport.test.tsx + * Unit tests for TaxReport (issue #605). + */ + +import { render, screen, waitFor } from "@testing-library/react"; +import TaxReport from "@/components/TaxReport"; + +jest.mock("react-i18next", () => ({ + useTranslation: () => ({ + t: (key: string) => key, + i18n: { language: "en" }, + }), +})); + +const mockPublicKey = { publicKey: null as string | null }; +jest.mock("@/lib/useWallet", () => ({ + useWallet: () => ({ publicKey: mockPublicKey.publicKey }), +})); + +const mockGetPaymentHistory = jest.fn(); +jest.mock("@/lib/stellar", () => ({ + getPaymentHistory: (...args: unknown[]) => mockGetPaymentHistory(...args), +})); + +jest.mock("@/lib/portfolio", () => { + const actual = jest.requireActual("@/lib/portfolio"); + return { + ...actual, + fetchTokenPricesCached: jest.fn().mockResolvedValue({ prices: {}, stale: false }), + fetchHistoricalPrices: jest.fn().mockResolvedValue({}), + }; +}); + +jest.mock("@/lib/exportTransactions", () => ({ + downloadCSV: jest.fn(), +})); + +describe("TaxReport", () => { + beforeEach(() => { + jest.clearAllMocks(); + mockPublicKey.publicKey = null; + mockGetPaymentHistory.mockReset(); + }); + + it("prompts to connect a wallet when none is connected", () => { + render(); + expect(screen.getByText("dashboard.connectPrompt")).toBeInTheDocument(); + }); + + it("shows an empty state when there is no transaction history", async () => { + mockPublicKey.publicKey = "GABC"; + mockGetPaymentHistory.mockResolvedValue({ records: [], nextCursor: undefined }); + + render(); + await waitFor(() => + expect(screen.getByText("taxReport.noData")).toBeInTheDocument() + ); + }); + + it("renders a summary and per-asset rows when history exists", async () => { + mockPublicKey.publicKey = "GABC"; + mockGetPaymentHistory.mockResolvedValue({ + records: [ + { + id: "1", + type: "received", + amount: "10", + asset: "XLM", + from: "GOTHER", + to: "GABC", + createdAt: "2026-01-01T00:00:00Z", + transactionHash: "abc", + hash: "abc", + }, + ], + nextCursor: undefined, + }); + + render(); + + await waitFor(() => + expect(screen.getByText("taxReport.summary")).toBeInTheDocument() + ); + // Asset row present + expect(screen.getByText("XLM")).toBeInTheDocument(); + // Export button present + expect(screen.getByText("taxReport.exportCSV")).toBeInTheDocument(); + }); +}); diff --git a/frontend/__tests__/portfolio.test.ts b/frontend/__tests__/portfolio.test.ts index b3e4c547..f53b0a67 100644 --- a/frontend/__tests__/portfolio.test.ts +++ b/frontend/__tests__/portfolio.test.ts @@ -24,6 +24,7 @@ import { recordPortfolioValueSnapshot, loadPortfolioHistory, calculatePnL, + computeFIFOTaxReport, } from "@/lib/portfolio"; const mockGetBalances = stellarModule.getBalances as jest.Mock; @@ -244,3 +245,94 @@ describe("portfolio value history and P&L", () => { expect(pnl).toEqual({ absolute: 0, percent: null }); }); }); + +describe("FIFO cost-basis tax reporting (issue #605)", () => { + it("builds a single lot for one received event and values it at acquisition price", () => { + const report = computeFIFOTaxReport( + [ + { type: "received", amount: "10", asset: "XLM", createdAt: "2026-01-01T00:00:00Z" }, + ], + (asset) => (asset === "XLM" ? 0.1 : null), + "USD" + ); + expect(report.positions).toHaveLength(1); + const pos = report.positions[0]; + expect(pos.asset).toBe("XLM"); + expect(pos.remainingQty).toBe(10); + expect(pos.costBasis).toBeCloseTo(1.0, 5); + expect(pos.unrealizedGain).toBeCloseTo(0, 5); + expect(report.totalRealizedGain).toBeCloseTo(0, 5); + }); + + it("computes realized gain on a FIFO disposal", () => { + const report = computeFIFOTaxReport( + [ + // Buy 10 XLM @ 1.00 on day 1 + { type: "received", amount: "10", asset: "XLM", createdAt: "2026-01-01T00:00:00Z" }, + // Sell 4 XLM @ 2.00 on day 2 -> realized gain = 4*(2-1) = 4 + { type: "sent", amount: "4", asset: "XLM", createdAt: "2026-01-02T00:00:00Z" }, + ], + (asset, date) => (asset === "XLM" ? (date.startsWith("2026-01-01") ? 1 : 2) : null), + "USD" + ); + expect(report.totalRealizedGain).toBeCloseTo(4, 5); + // Remaining 6 XLM @ cost basis 1.0 each = 6, valued @ 2.0 -> unrealized 6 + expect(report.positions[0].remainingQty).toBe(6); + expect(report.positions[0].costBasis).toBeCloseTo(6, 5); + expect(report.totalUnrealizedGain).toBeCloseTo(6, 5); + }); + + it("consumes the oldest lot first (FIFO order)", () => { + const report = computeFIFOTaxReport( + [ + // Buy 10 @ 1, then 10 @ 3 + { type: "received", amount: "10", asset: "XLM", createdAt: "2026-01-01T00:00:00Z" }, + { type: "received", amount: "10", asset: "XLM", createdAt: "2026-02-01T00:00:00Z" }, + // Sell 15 -> consumes all 10 of lot1 (cost 10) + 5 of lot2 (cost 15) = 25 + { type: "sent", amount: "15", asset: "XLM", createdAt: "2026-03-01T00:00:00Z" }, + ], + (asset, date) => { + if (date.startsWith("2026-01")) return 1; + if (date.startsWith("2026-02")) return 3; + return 5; // sell price + }, + "USD" + ); + expect(report.totalRealizedGain).toBeCloseTo(15 * 5 - 25, 5); // 75-25=50 + expect(report.positions[0].remainingQty).toBe(5); // 5 from lot2 remain + expect(report.positions[0].costBasis).toBeCloseTo(15, 5); // 5 * 3 + }); + + it("flags a position as unpriced when historical price is unavailable", () => { + const report = computeFIFOTaxReport( + [ + { type: "received", amount: "10", asset: "CUSTOM", createdAt: "2026-01-01T00:00:00Z" }, + ], + () => null, + "USD" + ); + expect(report.positions[0].unpriced).toBe(true); + expect(report.positions[0].costBasis).toBe(0); + }); + + it("is order-independent across the feed and handles swaps as paired events", () => { + // Swap 10 XLM -> 20 USDC on the same date: XLM sent, USDC received. + const feed = [ + { type: "received", amount: "10", asset: "XLM", createdAt: "2026-01-01T00:00:00Z" }, + { type: "sent", amount: "10", asset: "XLM", createdAt: "2026-01-05T00:00:00Z" }, + { type: "received", amount: "20", asset: "USDC", createdAt: "2026-01-05T00:00:00Z" }, + ]; + const priceAt = (asset: string, date: string) => { + if (asset === "XLM") return 1; // constant + if (asset === "USDC") return date.startsWith("2026-01") ? 1 : 1; + return null; + }; + const report = computeFIFOTaxReport([...feed].reverse(), priceAt, "USD"); + // Sorting inside the engine makes order irrelevant + expect(report.totalRealizedGain).toBeCloseTo(0, 5); + const xlm = report.positions.find((p) => p.asset === "XLM"); + const usdc = report.positions.find((p) => p.asset === "USDC"); + expect(xlm?.remainingQty).toBe(0); + expect(usdc?.remainingQty).toBe(20); + }); +}); diff --git a/frontend/components/TaxReport.tsx b/frontend/components/TaxReport.tsx new file mode 100644 index 00000000..15c2f1a4 --- /dev/null +++ b/frontend/components/TaxReport.tsx @@ -0,0 +1,311 @@ +/** + * components/TaxReport.tsx + * FIFO cost-basis tax report with CSV export (issue #605). + * + * Loads payment history + historical prices, computes FIFO cost basis + * and realized/unrealized gains, and renders a summary + per-asset detail. + */ +import clsx from "clsx"; +import { useCallback, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import Skeleton from "@/components/Skeleton"; +import { downloadCSV } from "@/lib/exportTransactions"; +import { + type FiatCurrency, + type TaxReport as TaxReportT, + type TaxEventInput, + COINGECKO_ID_MAP, + fetchTokenPricesCached, + fetchHistoricalPrices, + computeFIFOTaxReport, + historicalPriceLookup, +} from "@/lib/portfolio"; +import { getPaymentHistory } from "@/lib/stellar"; +import { useWallet } from "@/lib/useWallet"; +import { formatAmount } from "@/utils/format"; + +interface TaxReportProps { + fiatCurrency: FiatCurrency; +} + +export default function TaxReport({ fiatCurrency }: TaxReportProps) { + const { t, i18n } = useTranslation("common"); + const { publicKey } = useWallet(); + const [report, setReport] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const loadReport = useCallback(async () => { + if (!publicKey) { + setLoading(false); + return; + } + + setLoading(true); + setError(null); + + try { + // 1. Load all payment history + const events: TaxEventInput[] = []; + let cursor: string | undefined; + for (let i = 0; i < 10; i++) { + const { records, nextCursor } = await getPaymentHistory(publicKey, 200, cursor); + for (const r of records) { + events.push({ + type: r.type, + amount: r.amount, + asset: r.asset, + createdAt: r.createdAt, + }); + } + if (!nextCursor || records.length === 0) break; + cursor = nextCursor; + } + + if (events.length === 0) { + setReport(null); + setLoading(false); + return; + } + + // 2. Collect unique asset codes that have CoinGecko mappings + const codes = [...new Set(events.map((e) => e.asset))].filter( + (c) => COINGECKO_ID_MAP[c] + ); + + // 3. Load live prices (for recent dates and fallback) + const { prices: livePrices } = await fetchTokenPricesCached(codes); + + // 4. Load historical prices per asset + const history: Record> = {}; + await Promise.all( + codes.map(async (code) => { + const hist = await fetchHistoricalPrices(code, fiatCurrency, 365); + history[code] = hist; + }) + ); + + // 5. Build the lookup: live prices as fallback + const liveFlat: Record = {}; + for (const code of codes) { + const sp = livePrices[code]?.prices[fiatCurrency]; + if (sp !== undefined) liveFlat[code] = sp; + } + + const priceAt = historicalPriceLookup(history, liveFlat); + const result = computeFIFOTaxReport(events, priceAt, fiatCurrency); + setReport(result); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to load tax report"); + } finally { + setLoading(false); + } + }, [publicKey, fiatCurrency]); + + useEffect(() => { + void loadReport(); + }, [loadReport]); + + // ── CSV export ──────────────────────────────────────────────────────────── + const handleExportCSV = () => { + if (!report || report.positions.length === 0) return; + + const headers = [ + "Asset", + "Remaining Qty", + "Cost Basis", + "Unrealized Gain", + "Realized Gain", + "Total Acquired", + "Unpriced", + ]; + const rows = report.positions.map((p) => [ + p.asset, + p.remainingQty.toFixed(6), + p.costBasis.toFixed(2), + p.unrealizedGain.toFixed(2), + "—", // realized is per-report, not per-asset in current model + p.totalAcquired.toFixed(2), + p.unpriced ? "Yes" : "No", + ]); + + // Summary row + rows.push([ + "TOTAL", + "", + "", + report.totalUnrealizedGain.toFixed(2), + report.totalRealizedGain.toFixed(2), + "", + "", + ]); + + const bom = "\uFEFF"; + const csv = [headers, ...rows] + .map((row) => row.map((v) => `"${v.replace(/"/g, '""')}"`).join(",")) + .join("\n"); + + downloadCSV(bom + csv, `finchippay-tax-report-${new Date().toISOString().slice(0, 10)}.csv`); + }; + + // ── Render ──────────────────────────────────────────────────────────────── + + if (!publicKey) { + return ( +
+ {t("dashboard.connectPrompt")} +
+ ); + } + + if (loading) { + return ; + } + + if (error) { + return ( +
{error}
+ ); + } + + if (!report || report.positions.length === 0) { + return ( +
+ {t("taxReport.noData") || "No transaction history available for tax reporting."} +
+ ); + } + + return ( +
+ {/* Summary Card */} +
+
+

+ {t("taxReport.summary") || "Tax Summary (FIFO)"} +

+ +
+
+
+

+ {t("taxReport.realizedGain") || "Realized Gains"} +

+

= 0 + ? "text-green-600 dark:text-green-400" + : "text-red-600 dark:text-red-400" + )} + > + {report.totalRealizedGain >= 0 ? "+" : ""} + {formatAmount( + report.totalRealizedGain, + fiatCurrency, + i18n.language + )} +

+
+
+

+ {t("taxReport.unrealizedGain") || "Unrealized Gains"} +

+

= 0 + ? "text-green-600 dark:text-green-400" + : "text-red-600 dark:text-red-400" + )} + > + {report.totalUnrealizedGain >= 0 ? "+" : ""} + {formatAmount( + report.totalUnrealizedGain, + fiatCurrency, + i18n.language + )} +

+
+
+

+ {t("taxReport.positions") || "Assets"} +

+

+ {report.positions.length} +

+
+
+

+ {t("taxReport.generatedAt") || "Generated"}: {new Date(report.generatedAt).toLocaleDateString()} +  ·  + {t("taxReport.currency") || "Currency"}: {fiatCurrency} +

+
+ + {/* Per-asset Table */} +
+ + + + + + + + + + + + + {report.positions.map((p) => { + const gainColor = + p.unrealizedGain >= 0 + ? "text-green-600 dark:text-green-400" + : "text-red-600 dark:text-red-400"; + return ( + + + + + + + + + ); + })} + +
{t("taxReport.asset") || "Asset"}{t("taxReport.remaining") || "Remaining"}{t("taxReport.costBasis") || "Cost Basis"}{t("taxReport.unrealized") || "Unrealized"}{t("taxReport.lots") || "Lots"}{t("taxReport.priced") || "Priced"}
{p.asset} + {p.remainingQty.toFixed(4)} + + {formatAmount(p.costBasis, fiatCurrency, i18n.language)} + + {p.unrealizedGain >= 0 ? "+" : ""} + {formatAmount(p.unrealizedGain, fiatCurrency, i18n.language)} + {p.lots.length} + {p.unpriced ? ( + {t("taxReport.incomplete") || "⚠ Unpriced"} + ) : ( + + )} +
+
+ +

+ {t("taxReport.disclaimer") || ( + <> + Note: Cost basis uses FIFO (First-In, First-Out) and + CoinGecko historical prices. Realized gains are computed from sent + payments where historical prices are available. Swaps and trades are + not yet tracked as paired events. This report is for informational + purposes and should be verified by a tax professional. + + )} +

+
+ ); +} \ No newline at end of file diff --git a/frontend/lib/portfolio.ts b/frontend/lib/portfolio.ts index 957d7347..116ace6b 100644 --- a/frontend/lib/portfolio.ts +++ b/frontend/lib/portfolio.ts @@ -310,3 +310,224 @@ export function calculatePnL(currentValue: number, days: number): PnLResult { const percent = baseline.totalValue > 0 ? (absolute / baseline.totalValue) * 100 : null; return { absolute, percent }; } + +// ─── Tax reporting: FIFO cost basis (issue #605) ───────────────────────────── + +/** + * A single FIFO acquisition lot. `remaining` shrinks as later disposals + * consume it from the oldest lot first. + */ +export interface TaxLot { + asset: string; + /** ISO date (YYYY-MM-DD) the lot was acquired on. */ + acquiredAt: string; + /** Quantity acquired in this lot. */ + quantity: number; + /** Fiat cost per unit at acquisition (from historical price). */ + unitCost: number; + /** Quantity still held from this lot (after FIFO disposals). */ + remaining: number; +} + +export interface AssetTaxPosition { + asset: string; + lots: TaxLot[]; + /** Total fiat spent acquiring this asset (all lots). */ + totalAcquired: number; + /** Total fiat received from disposals (sales). */ + totalDisposed: number; + /** Remaining quantity still held. */ + remainingQty: number; + /** Fiat cost basis of the remaining held quantity. */ + costBasis: number; + /** Realized gain/loss from disposals. */ + realizedGain: number; + /** Unrealized gain/loss on the remaining held quantity. */ + unrealizedGain: number; + /** True when any lot lacked a historical price (cost basis is incomplete). */ + unpriced: boolean; +} + +export interface TaxReport { + currency: FiatCurrency; + positions: AssetTaxPosition[]; + totalRealizedGain: number; + totalUnrealizedGain: number; + generatedAt: string; +} + +/** Minimal shape the FIFO engine needs from a payment/operation record. */ +export interface TaxEventInput { + type: string; + amount: string; + asset: string; + createdAt: string; +} + +function toNumber(value: string): number { + const n = Number(value); + return Number.isFinite(n) ? n : 0; +} + +function dateKey(iso: string): string { + return (iso || "").slice(0, 10); +} + +/** + * Compute FIFO cost basis + realized/unrealized gains from a chronological + * feed of payment/operation events. + * + * - `received` events open a new acquisition lot priced at `priceAt(asset, date)`. + * - `sent` events dispose against the oldest remaining lot (FIFO); sale + * proceeds are also valued at `priceAt(asset, date)`. + * - Swap/trade operations are surfaced as paired sent (out) + received (in) + * records by the caller, so they are taxed naturally here. + * + * `priceAt` must return a fiat unit price for the asset on the given date, or + * `null` when unavailable. When a price is missing the position is flagged + * `unpriced` and that event's contribution to gains is skipped (cost basis is + * reported as incomplete rather than wrong). + */ +export function computeFIFOTaxReport( + records: TaxEventInput[], + priceAt: (asset: string, date: string) => number | null, + currency: FiatCurrency = "USD" +): TaxReport { + const byAsset = new Map(); + const unpricedAssets = new Set(); + let totalRealized = 0; + let totalUnrealized = 0; + + const sorted = [...records].sort((a, b) => + (a.createdAt || "").localeCompare(b.createdAt || "") + ); + + for (const rec of sorted) { + const asset = rec.asset; + const qty = toNumber(rec.amount); + if (qty <= 0) continue; + const date = dateKey(rec.createdAt); + const price = priceAt(asset, date); + const priced = price !== null && price > 0; + + if (!priced) unpricedAssets.add(asset); + + let lots = byAsset.get(asset); + if (!lots) { + lots = []; + byAsset.set(asset, lots); + } + + if (rec.type === "received") { + lots.push({ + asset, + acquiredAt: date, + quantity: qty, + unitCost: priced ? price : 0, + remaining: qty, + }); + } else if (rec.type === "sent") { + // Dispose FIFO from oldest lot. + let remainingToSell = qty; + let costBasisSold = 0; + while (remainingToSell > 1e-9 && lots.length > 0) { + const lot = lots[0]; + const take = Math.min(lot.remaining, remainingToSell); + costBasisSold += take * lot.unitCost; + lot.remaining -= take; + remainingToSell -= take; + if (lot.remaining <= 1e-9) lots.shift(); + } + if (priced) { + const proceeds = qty * price; + // If some quantity couldn't be matched to a lot (no prior buy), treat + // it as zero-cost for a conservative realized gain. + totalRealized += proceeds - costBasisSold; + } + } + } + + const positions: AssetTaxPosition[] = []; + // Find the latest event date for unrealized-gain valuation. + const latestDate = sorted.length > 0 ? dateKey(sorted[sorted.length - 1].createdAt) : ""; + for (const [asset, lots] of byAsset) { + const remainingQty = lots.reduce((s, l) => s + l.remaining, 0); + const costBasis = lots.reduce((s, l) => s + l.remaining * l.unitCost, 0); + const totalAcquired = lots.reduce((s, l) => s + l.quantity * l.unitCost, 0); + const unpriced = unpricedAssets.has(asset); + // Value remaining lots at the latest available price. + const currentPrice = priceAt(asset, latestDate); + const unrealizedGain = + currentPrice !== null && currentPrice > 0 && !unpriced + ? remainingQty * currentPrice - costBasis + : 0; + totalUnrealized += unrealizedGain; + + positions.push({ + asset, + lots, + totalAcquired, + totalDisposed: 0, + remainingQty, + costBasis, + realizedGain: 0, + unrealizedGain, + unpriced, + }); + } + + // Realized gain is tracked at the report level because it spans matched lots. + return { + currency, + positions, + totalRealizedGain: totalRealized, + totalUnrealizedGain: totalUnrealized, + generatedAt: new Date().toISOString(), + }; +} + +/** + * Fetch ~daily historical prices for a CoinGecko-mapped asset and return a + * lookup `dateKey(YYYY-MM-DD) -> price` for the requested fiat currency. + * Falls back to the live price for recent dates when the series is sparse. + */ +export async function fetchHistoricalPrices( + code: string, + currency: FiatCurrency, + days = 365 +): Promise> { + const geckoId = COINGECKO_ID_MAP[code]; + if (!geckoId) return {}; + const vs = VS_CURRENCY[currency]; + const url = `https://api.coingecko.com/api/v3/coins/${geckoId}/market_chart?vs_currency=${vs}&days=${days}`; + const res = await fetch(url); + if (!res.ok) return {}; + const data = (await res.json()) as { prices?: [number, number][] }; + const map: Record = {}; + for (const [ts, price] of data.prices || []) { + const d = new Date(ts).toISOString().slice(0, 10); + map[d] = price; + } + return map; +} + +/** Build a `priceAt` closure from a preloaded per-asset historical map. */ +export function historicalPriceLookup( + history: Record>, + live: Record +): (asset: string, date: string) => number | null { + return (asset, date) => { + const series = history[asset]; + if (series) { + if (series[date] !== undefined) return series[date]; + // Walk backwards a few days for weekend/holiday gaps. + const d = new Date(date + "T00:00:00Z"); + for (let i = 1; i <= 5; i++) { + d.setUTCDate(d.getUTCDate() - 1); + const k = d.toISOString().slice(0, 10); + if (series[k] !== undefined) return series[k]; + } + } + return live[asset] ?? null; + }; +} diff --git a/frontend/pages/portfolio.tsx b/frontend/pages/portfolio.tsx index cca84c31..8434b4cf 100644 --- a/frontend/pages/portfolio.tsx +++ b/frontend/pages/portfolio.tsx @@ -9,6 +9,7 @@ * Renders a "coming soon" fallback when the flag is off. */ +import clsx from "clsx"; import dynamic from "next/dynamic"; import Head from "next/head"; import Link from "next/link"; @@ -16,8 +17,10 @@ import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import PortfolioAllocation from "@/components/PortfolioAllocation"; import PortfolioOverview from "@/components/PortfolioOverview"; +import TaxReport from "@/components/TaxReport"; import TokenPriceChart from "@/components/TokenPriceChart"; import { FeatureGate } from "@/lib/FeatureFlags"; +import { logger } from "@/lib/logger"; import { getPortfolioHoldings, loadCustomTokens, @@ -32,7 +35,6 @@ import { type FiatCurrency, } from "@/lib/portfolio"; import { useWallet } from "@/lib/useWallet"; -import { logger } from "@/lib/logger"; const WalletConnect = dynamic(() => import("@/components/WalletConnect"), { ssr: false }); @@ -45,6 +47,7 @@ export default function PortfolioPage() { const [loading, setLoading] = useState(true); const [fiatCurrency, setFiatCurrency] = useState("USD"); const [selectedCode, setSelectedCode] = useState(null); + const [viewTab, setViewTab] = useState<"portfolio" | "tax">("portfolio"); const [addTokenInput, setAddTokenInput] = useState(""); const [addTokenError, setAddTokenError] = useState(null); @@ -203,22 +206,46 @@ export default function PortfolioPage() { {addTokenError &&

{addTokenError}

} + {/* Tab switcher: Portfolio overview ⇄ Tax report */} +
+ {(["portfolio", "tax"] as const).map((tab) => ( + + ))} +
+ -
- - - {selectedHolding && ( - - )} -
+ {viewTab === "portfolio" && ( +
+ + + {selectedHolding && ( + + )} +
+ )} + + {viewTab === "tax" && } )} diff --git a/frontend/public/locales/ar/common.json b/frontend/public/locales/ar/common.json index 09ef7d49..958c502e 100644 --- a/frontend/public/locales/ar/common.json +++ b/frontend/public/locales/ar/common.json @@ -182,7 +182,9 @@ "refreshPrices": "__MISSING_TRANSLATION__", "title": "المحفظة", "totalValue": "القيمة الإجمالية", - "valueOverTime": "__MISSING_TRANSLATION__" + "valueOverTime": "__MISSING_TRANSLATION__", + "tabOverview": "المحفظة", + "tabTaxReport": "تقرير الضرائب" }, "sendPayment": { "accountDoesNotExist": "هذا الحساب غير موجود على شبكة Stellar.", @@ -332,5 +334,23 @@ "title": "المدفوعات الأخيرة", "tryAgain": "حاول مرة أخرى", "viewOnExpert": "عرض على Stellar Expert" + }, + "taxReport": { + "summary": "ملخص الضرائب (FIFO)", + "exportCSV": "تصدير CSV", + "realizedGain": "أرباح محققة", + "unrealizedGain": "أرباح غير محققة", + "positions": "الأصول", + "generatedAt": "تم الإنشاء", + "currency": "العملة", + "asset": "الأصل", + "remaining": "المتبقي", + "costBasis": "أساس التكلفة", + "unrealized": "غير محقق", + "lots": "الدفعات", + "priced": "السعر", + "incomplete": "⚠ بدون سعر", + "noData": "لا يوجد سجل معاملات متاح لتقرير الضرائب.", + "disclaimer": "" } } diff --git a/frontend/public/locales/en/common.json b/frontend/public/locales/en/common.json index 858f470f..c47e36ba 100644 --- a/frontend/public/locales/en/common.json +++ b/frontend/public/locales/en/common.json @@ -182,7 +182,9 @@ "refreshPrices": "Refresh Prices", "title": "Portfolio", "totalValue": "Total Value", - "valueOverTime": "Portfolio Value Over Time" + "valueOverTime": "Portfolio Value Over Time", + "tabOverview": "Portfolio", + "tabTaxReport": "Tax Report" }, "sendPayment": { "accountDoesNotExist": "This account doesn't exist on the Stellar network.", @@ -332,5 +334,23 @@ "title": "Recent Payments", "tryAgain": "Try again", "viewOnExpert": "View on Stellar Expert" + }, + "taxReport": { + "summary": "Tax Summary (FIFO)", + "exportCSV": "Export CSV", + "realizedGain": "Realized Gains", + "unrealizedGain": "Unrealized Gains", + "positions": "Assets", + "generatedAt": "Generated", + "currency": "Currency", + "asset": "Asset", + "remaining": "Remaining", + "costBasis": "Cost Basis", + "unrealized": "Unrealized", + "lots": "Lots", + "priced": "Priced", + "incomplete": "⚠ Unpriced", + "noData": "No transaction history available for tax reporting.", + "disclaimer": "" } } diff --git a/frontend/public/locales/es/common.json b/frontend/public/locales/es/common.json index a0881940..92911ed7 100644 --- a/frontend/public/locales/es/common.json +++ b/frontend/public/locales/es/common.json @@ -182,7 +182,9 @@ "refreshPrices": "__MISSING_TRANSLATION__", "title": "Portafolio", "totalValue": "Valor Total", - "valueOverTime": "__MISSING_TRANSLATION__" + "valueOverTime": "__MISSING_TRANSLATION__", + "tabOverview": "Portafolio", + "tabTaxReport": "Reporte de Impuestos" }, "sendPayment": { "accountDoesNotExist": "Esta cuenta no existe en la red Stellar.", @@ -332,5 +334,23 @@ "title": "Pagos Recientes", "tryAgain": "Intentar de nuevo", "viewOnExpert": "Ver en Stellar Expert" + }, + "taxReport": { + "summary": "Resumen Fiscal (FIFO)", + "exportCSV": "Exportar CSV", + "realizedGain": "Ganancias Realizadas", + "unrealizedGain": "Ganancias No Realizadas", + "positions": "Activos", + "generatedAt": "Generado", + "currency": "Moneda", + "asset": "Activo", + "remaining": "Restante", + "costBasis": "Base de Costo", + "unrealized": "No Realizado", + "lots": "Lotes", + "priced": "Precio", + "incomplete": "⚠ Sin Precio", + "noData": "No hay historial de transacciones disponible para el informe fiscal.", + "disclaimer": "" } } diff --git a/frontend/public/locales/fr/common.json b/frontend/public/locales/fr/common.json index 6f84c785..36f70d88 100644 --- a/frontend/public/locales/fr/common.json +++ b/frontend/public/locales/fr/common.json @@ -182,7 +182,9 @@ "refreshPrices": "__MISSING_TRANSLATION__", "title": "Portefeuille", "totalValue": "Valeur Totale", - "valueOverTime": "__MISSING_TRANSLATION__" + "valueOverTime": "__MISSING_TRANSLATION__", + "tabOverview": "Portefeuille", + "tabTaxReport": "Rapport Fiscal" }, "sendPayment": { "accountDoesNotExist": "Ce compte n'existe pas sur le réseau Stellar.", @@ -332,5 +334,23 @@ "title": "Paiements Récents", "tryAgain": "Réessayer", "viewOnExpert": "Voir sur Stellar Expert" + }, + "taxReport": { + "summary": "Résumé Fiscal (FIFO)", + "exportCSV": "Exporter CSV", + "realizedGain": "Gains Réalisés", + "unrealizedGain": "Gains Non Réalisés", + "positions": "Actifs", + "generatedAt": "Généré", + "currency": "Devise", + "asset": "Actif", + "remaining": "Restant", + "costBasis": "Base de Coût", + "unrealized": "Non Réalisé", + "lots": "Lots", + "priced": "Prix", + "incomplete": "⚠ Sans Prix", + "noData": "Aucun historique de transactions disponible pour le rapport fiscal.", + "disclaimer": "" } } diff --git a/frontend/public/locales/he/common.json b/frontend/public/locales/he/common.json index f9dd275d..7abfd24d 100644 --- a/frontend/public/locales/he/common.json +++ b/frontend/public/locales/he/common.json @@ -182,7 +182,9 @@ "refreshPrices": "__MISSING_TRANSLATION__", "title": "תיק השקעות", "totalValue": "שווי כולל", - "valueOverTime": "__MISSING_TRANSLATION__" + "valueOverTime": "__MISSING_TRANSLATION__", + "tabOverview": "תיק", + "tabTaxReport": "דוח מס" }, "sendPayment": { "accountDoesNotExist": "החשבון הזה אינו קיים ברשת Stellar.", @@ -332,5 +334,23 @@ "title": "תשלומים אחרונים", "tryAgain": "נסה שוב", "viewOnExpert": "צפייה ב-Stellar Expert" + }, + "taxReport": { + "summary": "סיכום מס (FIFO)", + "exportCSV": "ייצוא CSV", + "realizedGain": "רווחים ממומשים", + "unrealizedGain": "רווחים לא ממומשים", + "positions": "נכסים", + "generatedAt": "נוצר", + "currency": "מטבע", + "asset": "נכס", + "remaining": "יתרה", + "costBasis": "בסיס עלות", + "unrealized": "לא ממומש", + "lots": "מנות", + "priced": "מחיר", + "incomplete": "⚠ ללא מחיר", + "noData": "אין היסטוריית עסקאות זמינה לדוח המס.", + "disclaimer": "" } } diff --git a/frontend/public/locales/ja/common.json b/frontend/public/locales/ja/common.json index d0cdf750..d0a195f3 100644 --- a/frontend/public/locales/ja/common.json +++ b/frontend/public/locales/ja/common.json @@ -168,7 +168,9 @@ "range90d": "90日", "noHoldings": "トークンはまだありません。", "priceUnavailable": "価格情報なし", - "priceHistoryUnavailable": "このトークンの価格履歴は利用できません。" + "priceHistoryUnavailable": "このトークンの価格履歴は利用できません。", + "tabOverview": "ポートフォリオ", + "tabTaxReport": "税務レポート" }, "sendPayment": { "title": "支払いを送信", @@ -305,5 +307,23 @@ "languageTitle": "言語", "languageDescription": "アプリケーションインターフェースの言語を選択してください。", "rtlSupportNote": "RTL サポート:アラビア語とヘブライ語のレイアウトはミラー表示されます。" + }, + "taxReport": { + "summary": "税務サマリー(FIFO)", + "exportCSV": "CSV書き出し", + "realizedGain": "実現損益", + "unrealizedGain": "未実現損益", + "positions": "資産", + "generatedAt": "生成日", + "currency": "通貨", + "asset": "資産", + "remaining": "残高", + "costBasis": "原価基準", + "unrealized": "未実現", + "lots": "ロット数", + "priced": "価格", + "incomplete": "⚠ 価格なし", + "noData": "税務レポートに利用できる取引履歴がありません。", + "disclaimer": "" } } diff --git a/frontend/public/locales/pt/common.json b/frontend/public/locales/pt/common.json index 52d2e2b7..05331e02 100644 --- a/frontend/public/locales/pt/common.json +++ b/frontend/public/locales/pt/common.json @@ -168,7 +168,9 @@ "range90d": "90D", "noHoldings": "Nenhum token no portfólio ainda.", "priceUnavailable": "Preço indisponível", - "priceHistoryUnavailable": "Histórico de preço indisponível para este token." + "priceHistoryUnavailable": "Histórico de preço indisponível para este token.", + "tabOverview": "Portfólio", + "tabTaxReport": "Relatório Fiscal" }, "sendPayment": { "title": "Enviar Pagamento", @@ -305,5 +307,23 @@ "languageTitle": "Idioma", "languageDescription": "Selecione seu idioma preferido para a interface do aplicativo.", "rtlSupportNote": "Suporte RTL: layouts em árabe são espelhados." + }, + "taxReport": { + "summary": "Resumo Fiscal (FIFO)", + "exportCSV": "Exportar CSV", + "realizedGain": "Ganhos Realizados", + "unrealizedGain": "Ganhos Não Realizados", + "positions": "Ativos", + "generatedAt": "Gerado", + "currency": "Moeda", + "asset": "Ativo", + "remaining": "Restante", + "costBasis": "Base de Custo", + "unrealized": "Não Realizado", + "lots": "Lotes", + "priced": "Preço", + "incomplete": "⚠ Sem Preço", + "noData": "Nenhum histórico de transações disponível para o relatório fiscal.", + "disclaimer": "" } }