feat(broker-operation): show operation type label in ledger entries

This commit is contained in:
2026-08-27 11:33:25 +03:00
parent 695cda0456
commit d53ce50e28
4 changed files with 91 additions and 20 deletions
@@ -16,10 +16,18 @@ type LedgerEntryRowProps = {
type PaymentPresentation = {
amount: string;
direction: "incoming" | "outgoing" | "neutral";
label: string;
tone: "positive" | "negative" | "neutral";
};
function entryLabelKey(operation: BrokerageOperation): string {
if (operation.category === "income")
return operation.type === "OPERATION_TYPE_COUPON"
? "ledger.entryLabel.income.coupon"
: "ledger.entryLabel.income.dividend";
return `ledger.category.${operation.category}`;
}
const LedgerEntryRow: FC<LedgerEntryRowProps> = (props) => {
const { operation } = props;
const { t } = useTranslation();
@@ -28,11 +36,9 @@ const LedgerEntryRow: FC<LedgerEntryRowProps> = (props) => {
operation.ticker ?? operation.instrumentType ?? t("ledger.noInstrument");
const logoSrc = imageFailed ? null : instrumentLogoUrl(operation.logoName);
const payment = paymentPresentation(operation);
const metadata = [
formatSnapshotTime(operation.occurredAt),
instrument,
t(`ledger.category.${operation.category}`),
].join(" · ");
const metadata = [formatSnapshotTime(operation.occurredAt), instrument].join(
" · ",
);
return (
<Paper className={classes.row} withBorder>
@@ -59,7 +65,7 @@ const LedgerEntryRow: FC<LedgerEntryRowProps> = (props) => {
{payment.amount}
</Text>
<Text className={classes[payment.tone]} size="sm">
{t(`ledger.direction.${payment.direction}`)}
{t(payment.label)}
</Text>
</Stack>
</Paper>
@@ -70,11 +76,15 @@ function paymentPresentation(
operation: BrokerageOperation,
): PaymentPresentation {
if (!operation.payment)
return { amount: "—", direction: "neutral", tone: "neutral" };
return {
amount: "—",
label: "ledger.noMoney",
tone: "neutral",
};
if (isZeroDecimal(operation.payment.value))
return {
amount: formatMoney(operation.payment),
direction: "neutral",
label: "ledger.noMoney",
tone: "neutral",
};
if (operation.payment.value.startsWith("-"))
@@ -83,12 +93,12 @@ function paymentPresentation(
...operation.payment,
value: operation.payment.value.slice(1),
})}`,
direction: "outgoing",
label: entryLabelKey(operation),
tone: "negative",
};
return {
amount: `+${formatMoney(operation.payment)}`,
direction: "incoming",
label: entryLabelKey(operation),
tone: "positive",
};
}
@@ -61,7 +61,7 @@ describe("OperationRow", () => {
expect(screen.queryByText("Количество: 2")).toBeNull();
});
it("labels a positive payment as a Russian incoming amount", () => {
it("labels a positive payment with its category type", () => {
render(
<MantineProvider>
<MemoryRouter>
@@ -75,10 +75,70 @@ describe("OperationRow", () => {
</MantineProvider>,
);
expect(screen.getByText("Поступление")).toBeInTheDocument();
expect(screen.getByText("Сделка")).toBeInTheDocument();
expect(screen.getByText("+15 ₽")).toBeInTheDocument();
});
it("labels a coupon income as Купон", () => {
render(
<MantineProvider>
<MemoryRouter>
<OperationRow
operation={{
...operation,
type: "OPERATION_TYPE_COUPON",
category: "income",
payment: { currency: "RUB", value: "8" },
}}
/>
</MemoryRouter>
</MantineProvider>,
);
expect(screen.getByText("Купон")).toBeInTheDocument();
expect(screen.getByText("+8 ₽")).toBeInTheDocument();
});
it("labels a dividend income as Дивиденд", () => {
render(
<MantineProvider>
<MemoryRouter>
<OperationRow
operation={{
...operation,
type: "OPERATION_TYPE_DIVIDEND",
category: "income",
payment: { currency: "RUB", value: "20" },
}}
/>
</MemoryRouter>
</MantineProvider>,
);
expect(screen.getByText("Дивиденд")).toBeInTheDocument();
expect(screen.getByText("+20 ₽")).toBeInTheDocument();
});
it("labels a fee as Комиссия", () => {
render(
<MantineProvider>
<MemoryRouter>
<OperationRow
operation={{
...operation,
type: "OPERATION_TYPE_SERVICE_FEE",
category: "fee",
payment: { currency: "RUB", value: "-3" },
}}
/>
</MemoryRouter>
</MantineProvider>,
);
expect(screen.getByText("Комиссия")).toBeInTheDocument();
expect(screen.getByText("−3 ₽")).toBeInTheDocument();
});
it("keeps a zero payment neutral", () => {
render(
<MantineProvider>
@@ -94,7 +154,7 @@ describe("OperationRow", () => {
);
expect(screen.getByText("Без движения денег")).toBeInTheDocument();
expect(screen.queryByText("Поступление")).toBeNull();
expect(screen.queryByText("Сделка")).toBeNull();
});
it("uses an instrument initial when the ledger entry has no logo", () => {
+2 -2
View File
@@ -465,7 +465,7 @@ describe("AccountPage states", () => {
expect(screen.getByText("Вторая")).toBeInTheDocument();
});
it("uses the ledger row for a recent outgoing operation", () => {
it("uses the ledger row for a recent outgoing trade operation", () => {
renderPage(
<AccountPageLoaded
{...loadedProps()}
@@ -495,7 +495,7 @@ describe("AccountPage states", () => {
/>,
);
expect(screen.getByText("Списание")).toBeInTheDocument();
expect(screen.getByText("Сделка")).toBeInTheDocument();
expect(screen.getByText("−12 ₽")).toBeInTheDocument();
});
+5 -4
View File
@@ -105,10 +105,11 @@ const ru = {
emptyText: "Измените период или фильтры.",
noMoney: "Без движения денег",
noInstrument: "Без инструмента",
direction: {
incoming: "Поступление",
outgoing: "Списание",
neutral: "Без движения денег",
entryLabel: {
income: {
coupon: "Купон",
dividend: "Дивиденд",
},
},
category: {
trade: "Сделка",