Skip to main content
Technical ManualLooking for step-by-step instructions? In the User Manual: Bank Accounts and StatementsReview Bank TransactionsBank RulesBank Feeds

Banking

The Banking module imports bank and card statements, categorizes their transactions with rules and by hand, and transfers them to the ledger. It replaces the legacy bank rules, matching engine, categorization workbench, saved sessions, transfer, duplicate resolution and transferred sessions (BANK-04 to BANK-11). The Bank feeds feature brings the transactions of banks connected through Plaid (BANK-01 to BANK-03, rebuilt server-side). Checks reconciliation (BANK-12) and the missing-information export (BANK-07) follow.

ProjectsCrestApps.Almohaseb.Banking (module), CrestApps.Almohaseb.Banking.Core (models, CSV, rules engine, posting rule, service)
FeatureCrestApps.Almohaseb.Banking (Banking), enabled by both business recipes; depends on Accounting, Purchases & payables and Sales & receivables
CollectionBanking

Data​

TypeKindHolds
AlmohasebBankAccountContent type (AlmohasebBankAccountPart), so a business can add fieldsName, LedgerAccountId (a non-system asset, or liability for a card), LastFour, IsActive
BankTransactionDocumentBankAccountId, BatchId, Date, Description, Amount (+ received, − spent), CheckNumber, ExternalId, Status (New, Categorized, Excluded, Transferred), Target (account:{id} or category:{id}), VendorId / CustomerId, Memo, RuleId (null when a person chose), Revision, TransferredUtc
BankImportBatchDocumentName, BankAccountId, Source (csv), FileName, FirstDate, LastDate, Imported, Skipped, ImportedUtc, ImportedBy (replaces the legacy saved sessions)
BankRuleDocumentName, Priority, Match (ContainsWords, Exact, StartsWith), Text, Direction, BankAccountId, MinAmount, MaxAmount, Exclude, Target, VendorId, CustomerId, Memo, ApplyOnce, IsActive, CreatedUtc

BankingMigrations (version 1) creates the type and BankTransactionIndex (BankAccountId, Date; BankAccountId, ExternalId; BatchId, Status; ItemId), BankImportBatchIndex and BankRuleIndex.

CSV import​

  • CsvReader.Parse (Imports, Imports.Core) reads RFC 4180 CSV (quoted fields, doubled quotes, line breaks in quotes, BOM), with the separator (comma, semicolon or tab) detected on the first line.
  • BankCsvMappingGuesser.Guess(headers) maps date, description, amount or debit/credit, and check number from the header names.
  • BankCsvImporter.Read(rows, mapping) returns BankLines and BankLineErrors (MissingColumns, BadDate, BadAmount, NoDescription). Amounts accept $, thousands separators, (12.00) and 12.00-; a signed column follows PositiveIsSpent, debit/credit columns become received − spent; zero lines are dropped; dates use the mapping's format or US and ISO formats.
  • GetExternalIds(account, lines) hashes the account, date, amount, description (upper case, spaces collapsed) and the occurrence of identical lines within the file. ImportAsync skips ids already in the account, so overlapping statements and re-imports add nothing, while equal lines in one file stay distinct. The legacy sessions stored no provider or account id and could not dedupe (BANK-08).

Rules engine​

BankRuleMatcher is pure:

  • Tokenize splits text into upper-case words of letters and digits. Matching is on whole words, case-insensitive (the legacy matched substrings, case-sensitive).
  • IsMatch checks the rule is active, the direction, the account, the absolute amount range, then the text by Match.
  • Choose orders matches by Priority, then specificity (Exact, StartsWith, ContainsWords), then word count (descending), then CreatedUtc, then id. An FsCheck property checks the winner does not depend on the order of the rules (the legacy took the alphabetically last title, BANK-05).
  • SuggestText returns the non-numeric words every selected description shares (the legacy ExtractMutualWords).

ApplyRulesAsync and imports apply rules only to New transactions and to those a rule categorized (RuleId set); a person's choice is never overridden. An ApplyOnce rule is used for one transaction per run and then turned off (the legacy never cleaned these up after a Plaid import).

Review, transfer and undo​

IBankingService / BankingService:

  • CategorizeAsync(id, target, party, memo) validates the target: an active account that is not the bank's own (TargetIsBank), a purchase category for money spent or a sales category for money received (TargetWrongDirection). A null target sets it back to New. ExcludeAsync toggles Excluded.
  • TransferAsync(ids, keepBoth) posts each categorized transaction. Money spent is first checked by FindDuplicatesAsync: any bill or vendor payment (PayableIndex) with the same total within ±14 days (DuplicateWindowDays, the legacy heuristic, now including payments). A match returns PossibleDuplicate with the entries unless the id is in keepBoth. Outcomes: Transferred, PossibleDuplicate, NotCategorized, Rejected (posting errors, such as a closed period).
  • UndoTransferAsync posts the transaction as not transferred, which reverses its entry, and sets it back to Categorized. The legacy undo used three transactions and could leave a partial undo (BANK-11).
  • DeleteBatchAsync deletes a batch and its transactions, refused while any is transferred.

BankTransactionPostingRule (Banking.Transaction) posts only Transferred transactions: money spent Dr the target, Cr the bank's ledger account; money received the reverse. A category target resolves to its account, or to the Purchases or Sales system account. The legacy transfer wrote source documents only, by direct table writes, with no ledger (BANK-09).

Admin​

The Banking menu (almohasebBanking, icon fa-building-columns): Review transactions, Import a statement, Imports, Bank rules and Bank accounts (the content list).

Route (under the admin prefix)Route nameMethodsPermission
Almohaseb/BankingAlmohasebBankReview, AlmohasebBankReviewPostGET (bankAccountId, batchId, status, from, to, search; at most 200 rows), POST (Rows[i], command = save, rules, exclude, include, transfer, transferAnyway, undo, rule)View; Manage to post
Almohaseb/Banking/Import (+ /Upload, /Confirm)AlmohasebBankImport, ...GET, POST multipart (5 MB), POST (preview)Manage
Almohaseb/Banking/Imports (+ /Delete/{id})AlmohasebBankBatches, AlmohasebBankBatchDeleteGET, POSTView; Manage to delete
Almohaseb/Banking/Rules (+ /Create (fromIds), /Edit/{id}, /Save (preview), /Delete/{id})AlmohasebBankRules, ...GET, POSTView; Manage to change

Every POST of the review form first saves what was entered in the rows. BankingLists builds the pick-lists: purchase or sales categories, then every active account grouped by type.

Bank feeds (Plaid)​

Feature: CrestApps.Almohaseb.Banking.Plaid (Bank feeds), enabled by both business recipes; depends on Banking and Jobs. Code: CrestApps.Almohaseb.Banking.Core/Plaid (client, verifier, store, service) and CrestApps.Almohaseb.Banking/Plaid (startup, controller, job, webhook).

The platform has one Plaid account, configured in the host (see Configuration); each business keeps its own connections. Without a client ID and secret, Bank feeds says so and nothing calls Plaid.

Data​

BankConnection (Banking collection, BankConnectionIndex on PlaidItemId and Status, stored by BankConnectionStore, a CrestApps DocumentCatalog): the Plaid item id, ProtectedAccessToken (ISensitiveDataProtector, purpose CrestApps.Almohaseb.Banking.PlaidAccessToken, so only the business's own key ring reads it; it is never shown, exported or logged), the bank's id and name, Status (Active, NeedsReconnect, Error), LastError, LastSyncUtc, and Accounts: each Plaid account with its name, mask, kind, BankAccountId (the bank account it feeds, or none), ImportFrom, its own sync Cursor, and the last balances.

BankTransaction gains ChangedAtBank and RemovedAtBank; a feed transaction's ExternalId is plaid:{transaction_id}, so it comes in once.

Flow​

  1. Link. LinkToken (POST, JSON) asks /link/token/create for a token: products transactions, days_requested 730, the user's id as client_user_id, and the webhook https://{host}{path base}/almohaseb/plaid/webhook when the site is served over https on a host Plaid can reach (none on localhost). With a connection id it is update mode (the access token, no products). The page opens Plaid Link (cdn.plaid.com/link/v2/stable/link-initialize.js).
  2. Connect. Link's public token is posted to Connect; /item/public_token/exchange gives the access token, protected at once; /accounts/get and /institutions/get_by_id give the accounts and the bank's name. The same Plaid item connected again keeps its links and cursors.
  3. Link accounts. Accounts/{connectionId} saves each account's bank account and Bring in from date (LinkAccountsAsync): a bank account is fed by one Plaid account across every connection; a changed link or date clears the account's cursor, and the external ids keep transactions from coming twice.
  4. Sync. BankFeedSyncJobHandler (job type CrestApps.Almohaseb.Banking.BankFeedSync, idempotency key bank-feed:{connection id}, batches of 10 pages) calls IBankFeedService.SyncAsync: for each linked account, /transactions/sync with the account's cursor (options.account_id, include_original_description, 500 a page). Each page goes through IBankingService.ApplyFeedAsync and moves the cursor in the same unit of work. TRANSACTIONS_SYNC_MUTATION_DURING_PAGINATION asks the page again from the cursor applied last. The balances are read again after the sync.
  5. Apply. ApplyFeedAsync: posted transactions only (a pending one returns under another id when it posts), on or after Bring in from, with Plaid's sign turned (Plaid's amounts are positive when money leaves). New ones are added to a batch (source feed) and the bank rules applied; changed ones are updated while in review, removed ones deleted; a transferred one is flagged ChangedAtBank or RemovedAtBank, and its entry is left alone.
  6. Errors. A Plaid error that needs the person (ITEM_LOGIN_REQUIRED, PENDING_EXPIRATION, ...) sets NeedsReconnect; any other sets Error, and the next sync tries again. Sign in again opens Link in update mode, then Reconnected/{connectionId} reads the accounts and queues a sync.

Webhook​

POST {site}/almohaseb/plaid/webhook (minimal API, anonymous, no antiforgery, excluded from the API description; 64 KB at most). PlaidWebhookVerifier checks the Plaid-Verification JWT: ES256, the key of its kid from /webhook_verification_key/get (cached an hour; an expired key is refused), a signature over the header and payload, iat at most five minutes old, and request_body_sha256 equal to the SHA-256 of the body received; anything else is 401. HandleWebhookAsync then queues a sync on TRANSACTIONS SYNC_UPDATES_AVAILABLE (and the older update codes) and ITEM LOGIN_REPAIRED, sets NeedsReconnect on ITEM ERROR with ITEM_LOGIN_REQUIRED, PENDING_EXPIRATION, PENDING_DISCONNECT, USER_PERMISSION_REVOKED and USER_ACCOUNT_REVOKED, and notes NEW_ACCOUNTS_AVAILABLE.

BankFeedBackgroundTask (06:15 each day) queues a sync of every connection with a linked account that does not need a sign-in, in case a webhook was missed.

Removing​

Remove/{connectionId} calls /item/remove (an item Plaid no longer knows is logged and removed here anyway) and deletes the connection; its transactions stay.

Route (under the admin prefix)Route nameMethodsPermission
Almohaseb/Banking/FeedsAlmohasebBankFeedsGETManage bank connections
Almohaseb/Banking/Feeds/LinkTokenAlmohasebBankFeedsLinkTokenPOST (connectionId), JSON { linkToken }Manage bank connections
Almohaseb/Banking/Feeds/ConnectAlmohasebBankFeedsConnectPOST (publicToken)Manage bank connections
Almohaseb/Banking/Feeds/Accounts/{connectionId}AlmohasebBankFeedsAccountsPOST (accounts[i].PlaidAccountId, BankAccountId, ImportFrom)Manage bank connections
Almohaseb/Banking/Feeds/Sync/{connectionId}, /Reconnected/{connectionId}, /Remove/{connectionId}AlmohasebBankFeedsSync, ...POSTManage bank connections

Permissions​

PermissionKeyImplied by
Import, categorize and transfer bank transactions, and keep the bank rulesManageAlmohasebBanking
View bank transactions and bank rulesViewAlmohasebBankingManageAlmohasebBanking
Connect banks through Plaid and link their accounts to the booksManageAlmohasebBankConnections (Bank feeds)

Bank accounts use the content permissions of the AlmohasebBankAccount type.

See also​