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 = { type PaymentPresentation = {
amount: string; amount: string;
direction: "incoming" | "outgoing" | "neutral"; label: string;
tone: "positive" | "negative" | "neutral"; 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 LedgerEntryRow: FC<LedgerEntryRowProps> = (props) => {
const { operation } = props; const { operation } = props;
const { t } = useTranslation(); const { t } = useTranslation();
@@ -28,11 +36,9 @@ const LedgerEntryRow: FC<LedgerEntryRowProps> = (props) => {
operation.ticker ?? operation.instrumentType ?? t("ledger.noInstrument"); operation.ticker ?? operation.instrumentType ?? t("ledger.noInstrument");
const logoSrc = imageFailed ? null : instrumentLogoUrl(operation.logoName); const logoSrc = imageFailed ? null : instrumentLogoUrl(operation.logoName);
const payment = paymentPresentation(operation); const payment = paymentPresentation(operation);
const metadata = [ const metadata = [formatSnapshotTime(operation.occurredAt), instrument].join(
formatSnapshotTime(operation.occurredAt), " · ",
instrument, );
t(`ledger.category.${operation.category}`),
].join(" · ");
return ( return (
<Paper className={classes.row} withBorder> <Paper className={classes.row} withBorder>
@@ -59,7 +65,7 @@ const LedgerEntryRow: FC<LedgerEntryRowProps> = (props) => {
{payment.amount} {payment.amount}
</Text> </Text>
<Text className={classes[payment.tone]} size="sm"> <Text className={classes[payment.tone]} size="sm">
{t(`ledger.direction.${payment.direction}`)} {t(payment.label)}
</Text> </Text>
</Stack> </Stack>
</Paper> </Paper>
@@ -70,11 +76,15 @@ function paymentPresentation(
operation: BrokerageOperation, operation: BrokerageOperation,
): PaymentPresentation { ): PaymentPresentation {
if (!operation.payment) if (!operation.payment)
return { amount: "—", direction: "neutral", tone: "neutral" }; return {
amount: "—",
label: "ledger.noMoney",
tone: "neutral",
};
if (isZeroDecimal(operation.payment.value)) if (isZeroDecimal(operation.payment.value))
return { return {
amount: formatMoney(operation.payment), amount: formatMoney(operation.payment),
direction: "neutral", label: "ledger.noMoney",
tone: "neutral", tone: "neutral",
}; };
if (operation.payment.value.startsWith("-")) if (operation.payment.value.startsWith("-"))
@@ -83,12 +93,12 @@ function paymentPresentation(
...operation.payment, ...operation.payment,
value: operation.payment.value.slice(1), value: operation.payment.value.slice(1),
})}`, })}`,
direction: "outgoing", label: entryLabelKey(operation),
tone: "negative", tone: "negative",
}; };
return { return {
amount: `+${formatMoney(operation.payment)}`, amount: `+${formatMoney(operation.payment)}`,
direction: "incoming", label: entryLabelKey(operation),
tone: "positive", tone: "positive",
}; };
} }
@@ -61,7 +61,7 @@ describe("OperationRow", () => {
expect(screen.queryByText("Количество: 2")).toBeNull(); 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( render(
<MantineProvider> <MantineProvider>
<MemoryRouter> <MemoryRouter>
@@ -75,10 +75,70 @@ describe("OperationRow", () => {
</MantineProvider>, </MantineProvider>,
); );
expect(screen.getByText("Поступление")).toBeInTheDocument(); expect(screen.getByText("Сделка")).toBeInTheDocument();
expect(screen.getByText("+15 ₽")).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", () => { it("keeps a zero payment neutral", () => {
render( render(
<MantineProvider> <MantineProvider>
@@ -94,7 +154,7 @@ describe("OperationRow", () => {
); );
expect(screen.getByText("Без движения денег")).toBeInTheDocument(); expect(screen.getByText("Без движения денег")).toBeInTheDocument();
expect(screen.queryByText("Поступление")).toBeNull(); expect(screen.queryByText("Сделка")).toBeNull();
}); });
it("uses an instrument initial when the ledger entry has no logo", () => { 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(); 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( renderPage(
<AccountPageLoaded <AccountPageLoaded
{...loadedProps()} {...loadedProps()}
@@ -495,7 +495,7 @@ describe("AccountPage states", () => {
/>, />,
); );
expect(screen.getByText("Списание")).toBeInTheDocument(); expect(screen.getByText("Сделка")).toBeInTheDocument();
expect(screen.getByText("−12 ₽")).toBeInTheDocument(); expect(screen.getByText("−12 ₽")).toBeInTheDocument();
}); });
+5 -4
View File
@@ -105,10 +105,11 @@ const ru = {
emptyText: "Измените период или фильтры.", emptyText: "Измените период или фильтры.",
noMoney: "Без движения денег", noMoney: "Без движения денег",
noInstrument: "Без инструмента", noInstrument: "Без инструмента",
direction: { entryLabel: {
incoming: "Поступление", income: {
outgoing: "Списание", coupon: "Купон",
neutral: "Без движения денег", dividend: "Дивиденд",
},
}, },
category: { category: {
trade: "Сделка", trade: "Сделка",