refactor: enforce frontend view model boundaries
This commit is contained in:
@@ -22,6 +22,7 @@ describe("useBrokerageOperations", () => {
|
||||
cursor: "next",
|
||||
}),
|
||||
).toEqual({ categories: ["income", "tax"], cursor: "next" });
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useBrokerageOperations("account-1", { limit: 5 }),
|
||||
{ wrapper: createQueryWrapper().wrapper },
|
||||
|
||||
@@ -8,7 +8,9 @@ type BrokerageOperationsQuery = Omit<
|
||||
NonNullable<OperationsEndpoint["parameters"]["query"]>,
|
||||
"sync"
|
||||
>;
|
||||
|
||||
type OperationCategory = BrokerageOperation["category"];
|
||||
|
||||
type OperationsEndpoint
|
||||
= paths["/api/v1/brokerage-accounts/{accountId}/operations"]["get"];
|
||||
|
||||
@@ -42,7 +44,11 @@ function brokerageOperationsQueryParameters(query: BrokerageOperationsQuery) {
|
||||
...(query.instrumentUid && { instrumentUid: query.instrumentUid }),
|
||||
...(query.from && { from: query.from }),
|
||||
...(query.to && { to: query.to }),
|
||||
...(query.categories?.length && { categories: query.categories.toSorted((left, right) => left.localeCompare(right)) }),
|
||||
...(query.categories?.length && {
|
||||
categories: query.categories.toSorted((left, right) =>
|
||||
left.localeCompare(right),
|
||||
),
|
||||
}),
|
||||
...(query.cursor && { cursor: query.cursor }),
|
||||
...(query.limit && { limit: query.limit }),
|
||||
};
|
||||
|
||||
@@ -22,7 +22,9 @@ const operationTransferPrefix = "OPERATION_TYPE_TRANS_";
|
||||
type PaymentPresentation = {
|
||||
amount: string;
|
||||
label: string;
|
||||
tone: "negative" | "neutral" | "positive";
|
||||
tone: "negative"
|
||||
| "neutral"
|
||||
| "positive";
|
||||
};
|
||||
|
||||
function entryLabelKey(operation: BrokerageOperation): string {
|
||||
@@ -30,6 +32,7 @@ function entryLabelKey(operation: BrokerageOperation): string {
|
||||
return isOperationType(operation.type, OperationType.Coupon)
|
||||
? "ledger.entryLabel.income.coupon"
|
||||
: "ledger.entryLabel.income.dividend";
|
||||
|
||||
return `ledger.category.${operation.category}`;
|
||||
}
|
||||
|
||||
@@ -43,19 +46,15 @@ function isZeroDecimal(value: string): boolean {
|
||||
|
||||
function operationLabelKey(type: string): string {
|
||||
if (
|
||||
[
|
||||
OperationType.Buy,
|
||||
OperationType.BuyCard,
|
||||
OperationType.BuyMargin,
|
||||
].some(candidate => isOperationType(type, candidate))
|
||||
[OperationType.Buy, OperationType.BuyCard, OperationType.BuyMargin].some(
|
||||
candidate => isOperationType(type, candidate),
|
||||
)
|
||||
)
|
||||
return "ledger.operationType.buy";
|
||||
if (
|
||||
[
|
||||
OperationType.Sell,
|
||||
OperationType.SellCard,
|
||||
OperationType.SellMargin,
|
||||
].some(candidate => isOperationType(type, candidate))
|
||||
[OperationType.Sell, OperationType.SellCard, OperationType.SellMargin].some(
|
||||
candidate => isOperationType(type, candidate),
|
||||
)
|
||||
)
|
||||
return "ledger.operationType.sell";
|
||||
if (
|
||||
@@ -72,7 +71,10 @@ function operationLabelKey(type: string): string {
|
||||
return "ledger.operationType.input";
|
||||
if (isOperationType(type, OperationType.OutputSecurities))
|
||||
return "ledger.operationType.output";
|
||||
return type.startsWith(operationTransferPrefix) ? "ledger.operationType.transfer" : "ledger.operationType.other";
|
||||
|
||||
return type.startsWith(operationTransferPrefix)
|
||||
? "ledger.operationType.transfer"
|
||||
: "ledger.operationType.other";
|
||||
}
|
||||
|
||||
function paymentPresentation(
|
||||
@@ -99,6 +101,7 @@ function paymentPresentation(
|
||||
label: entryLabelKey(operation),
|
||||
tone: "negative",
|
||||
};
|
||||
|
||||
return {
|
||||
amount: `+${formatMoney(operation.payment)}`,
|
||||
label: entryLabelKey(operation),
|
||||
|
||||
@@ -24,50 +24,59 @@ type LedgerEntryRowProps = {
|
||||
const LedgerEntryRow: FC<LedgerEntryRowProps> = (props) => {
|
||||
const { operation } = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
const instrument
|
||||
= operation.ticker ?? operation.instrumentType ?? t("ledger.noInstrument");
|
||||
|
||||
const payment = paymentPresentation(operation);
|
||||
|
||||
const metadata = [
|
||||
t("ledger.occurredAt", { date: formatSnapshotTime(operation.occurredAt) }),
|
||||
instrument,
|
||||
props.withDetails ? /* eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model. */ null : operation.account?.name,
|
||||
props.withDetails
|
||||
? null // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
|
||||
: operation.account?.name,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · ");
|
||||
|
||||
let details;
|
||||
if (props.withDetails) {
|
||||
details = (
|
||||
<>
|
||||
<Group gap="sm">
|
||||
{operation.quantity != undefined && (
|
||||
<Text c="dimmed" size="xs">
|
||||
{t("ledger.quantity", {
|
||||
value: formatDecimal(operation.quantity),
|
||||
})}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{!!operation.price && (
|
||||
<Text c="dimmed" size="xs">
|
||||
{t("ledger.price", { value: formatMoney(operation.price) })}
|
||||
</Text>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
{!!operation.account && (
|
||||
<Anchor
|
||||
component={Link}
|
||||
size="sm"
|
||||
to={`/accounts/${encodeURIComponent(operation.account.id)}`}
|
||||
>
|
||||
{operation.account.name}
|
||||
</Anchor>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
else details = undefined;
|
||||
|
||||
return (
|
||||
<EntryCard
|
||||
details={
|
||||
(props.withDetails)
|
||||
? ((
|
||||
<>
|
||||
<Group gap="sm">
|
||||
{operation.quantity != undefined && ((
|
||||
<Text c="dimmed" size="xs">
|
||||
{t("ledger.quantity", {
|
||||
value: formatDecimal(operation.quantity),
|
||||
})}
|
||||
</Text>
|
||||
))}
|
||||
{!!(operation.price) && ((
|
||||
<Text c="dimmed" size="xs">
|
||||
{t("ledger.price", { value: formatMoney(operation.price) })}
|
||||
</Text>
|
||||
))}
|
||||
</Group>
|
||||
{!!(operation.account) && ((
|
||||
<Anchor
|
||||
component={Link}
|
||||
size="sm"
|
||||
to={`/accounts/${encodeURIComponent(operation.account.id)}`}
|
||||
>
|
||||
{operation.account.name}
|
||||
</Anchor>
|
||||
))}
|
||||
</>
|
||||
))
|
||||
: undefined
|
||||
}
|
||||
details={details}
|
||||
imageSrc={instrumentLogoUrl(operation.logoName)}
|
||||
initial={(instrument.trim()[0] ?? "?").toUpperCase()}
|
||||
metadata={metadata}
|
||||
|
||||
Reference in New Issue
Block a user