UNPKG

@accounter/server

Version:
128 lines (127 loc) 7.31 kB
import DataLoader from 'dataloader'; import { TenantAwareDBClient } from '../../app-providers/tenant-db-client.js'; import { FinancialAccountsProvider } from '../../financial-accounts/providers/financial-accounts.provider.js'; import { FinancialBankAccountsProvider } from '../../financial-accounts/providers/financial-bank-accounts.provider.js'; import { TransactionsProvider } from '../../transactions/providers/transactions.provider.js'; import { type MatchableTransaction } from '../helpers/match-security-executions.helper.js'; import type { ChargeSecurityProto, PaginatedSecurityExecutionsProto, SecurityExecutionRow, SecurityExecutionsFilterInput } from '../types.js'; import { SecurityBusinessesProvider } from './security-businesses.provider.js'; /** * How many securities `includeCharges` will pair at once. * * Each one costs its whole execution history plus a transactions query, because the pairing is * only correct over a complete set (see `matchExecutionsToTransactions`). The cap is what keeps * "every trade I ever made, with charges" from turning into a portfolio-wide fan-out. */ export declare const MAX_CHARGE_LINK_SECURITIES = 10; /** What the reverse match needs off a transaction, charge included so a row can link out. */ type MatchedTransaction = MatchableTransaction & { charge_id: string; }; export declare class ForeignSecuritiesProvider { private db; private transactionsProvider; private financialAccountsProvider; private financialBankAccountsProvider; private securityBusinessesProvider; constructor(db: TenantAwareDBClient, transactionsProvider: TransactionsProvider, financialAccountsProvider: FinancialAccountsProvider, financialBankAccountsProvider: FinancialBankAccountsProvider, securityBusinessesProvider: SecurityBusinessesProvider); private batchSecuritiesByKeys; securityByKeyLoader: DataLoader<string, import("../types.js").IGetSecuritiesByKeysResult | null, string>; /** * The Poalim identity (bank/branch/account) of each account the given transactions touch. * `financial_accounts.account_number` is text while the poalim_* tables store it as an * integer, so non-numeric account numbers (and non-bank accounts, which have no * financial_bank_accounts row) simply drop out — they can never match an execution. */ private getAccountTuples; /** * Ingested portfolio executions matched to the charge's transactions, grouped by security * key. Returns an empty map when the charge touches no resolvable Poalim account, so a * charge whose accounts predate the bank-account backfill degrades to "no activity" rather * than erroring. */ private getMatchedExecutions; /** * The whole ingested life of one security business: every execution of every Poalim key it * is known by, each carrying the cash movement (and so the charge) behind it. * * The candidate transactions are the security business's own — which is what the counterparty * now is for a resolved trade — so this reads the same pairing the charge view shows, from * the other end. */ getSecurityBusinessHistory(businessId: string, ownerId: string): Promise<{ executions: import("../types.js").IGetSecurityExecutionsByKeysResult[]; transactionByExecutionId: Map<string, MatchedTransaction>; }>; /** * Every ingested execution of every security business the tenant has, grouped by the business * it belongs to. Each business gets an entry, so "nothing ingested" is distinguishable from * "not a security". * * One query for the whole portfolio: `getSecurityExecutionsByKeys` already filters on * `security = ANY(...)` and returns the key on every row, so the union of every business's * Poalim keys can be asked for at once and split back up in memory. * * Unlike `getSecurityBusinessHistory` this never looks at transactions or accounts — those * exist only to pair an execution with the cash movement behind it, which a position does not * need, and they cost a transactions query plus an account lookup per security. */ getExecutionsBySecurityBusiness(): Promise<Map<string, SecurityExecutionRow[]>>; /** * Poalim security key -> the security business it belongs to, for the given businesses. * * The executions table is keyed by the bank's proprietary key, and one business can carry * several of them — that is what the identifiers bridge is for — so every read of the * executions feed has to invert the relation first. The unique index on * `(owner_id, identifier_type, identifier_value)` is what makes one key resolve to exactly one * business, so a plain Map is enough. * * One batched query behind the loader, not one per business. */ private getBusinessIdBySecurityKey; /** * Which securities a filter names. * * `securityBusinessIds`, `isins` and `symbols` are three ways of naming the same axis, so they * union with each other rather than intersecting — asking for one ISIN and one symbol means * both securities, not the empty overlap. Naming none of them means every security. * * Resolved against the request-memoized `getAllSecurityBusinesses()` rather than with three * more queries: it is one round trip already paid for, and going through it means an id that * is not a security business of this tenant resolves to nothing instead of reaching the * executions feed. */ private resolveFilterSecurityBusinessIds; /** * A page of executions across securities, newest first. * * Two paths, because charge links and pagination do not compose. Without them the filter * pushes straight into SQL and the page is a `LIMIT`/`OFFSET` slice. With them the pairing has * to see a security's *whole* history — `matchExecutionsToTransactions` is greedy and * one-to-one over the sets it is handed, so pairing a page's slice would let an execution on * page 2 claim the cash movement that belongs to one on page 1, and the same execution would * report a different charge at a different page size. So that path reuses * `getSecurityBusinessHistory` per security, unpaginated, and slices in memory — which is why * it is capped at {@link MAX_CHARGE_LINK_SECURITIES} securities. */ getSecurityExecutionsPage(params: { filters: SecurityExecutionsFilterInput; page: number; limit: number; includeCharges: boolean; ownerId: string; }): Promise<PaginatedSecurityExecutionsProto>; /** * The `includeCharges` path: every named security's complete history, paired, then filtered, * ordered and sliced in memory. See {@link getSecurityExecutionsPage} for why it cannot be * done in SQL. */ private chargeLinkedExecutionsPage; /** * The securities a charge's transactions reference, keyed off the security key each * description carries. Keys with no ingested row are still returned, with a null * `details`, so a stale or missing scrape is visible instead of silently dropping data. */ getChargeSecurities(chargeId: string): Promise<ChargeSecurityProto[]>; } export {};