fix(reconciliation): exclude ignored transactions from the bank total and bridge whitespace-drifted duplicate descriptions (#1705)
Bank reconciliation counted ignored transactions in bank_transaction_total while excluding them from the unmatched count, so after the sanctioned duplicate cleanup (ignore one twin) the differens showed the ignored sum forever and is_reconciled was unreachable: observed live as a permanent 116 367 kr differens on a fully booked enskild firma (78 867 kr ignored reconnect duplicates + 37 500 kr genuinely unbooked). The ignore toast already promised 'försvinner från avstämningen'; now the engine keeps that promise. Ignored rows are surfaced separately (count + sum) in the status object, the UI card, and the v1 API, mirroring the IB pattern. The duplicates themselves came from a PSD2 reconnect: the new connection re-rendered identical transactions with drifted whitespace (CRLF vs space, and a DROPPED space), so the prefix-containment content bridge missed every twin. descriptionsBridge now strips all whitespace before comparing: char-filtering preserves existing prefix relations, and the compare stays confined to a (date, öre) bucket. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1082,4 +1082,5 @@ One line per decision: `[YYYY-MM-DD] <decision>: <why>`. Appended by agents and
|
||||
[2026-08-18] Migration-reset eligibility treats every persisted Skatteverket VAT `submission_*` workflow row as authority interaction evidence, not only successful audit-log rows: historical direct locks stored the signing state without auditing it, and Accounted cannot observe whether a user completed BankID signing outside the app. Unsigned drafts must be removed through the product; locked or uncertain state fails closed and is escalated.
|
||||
[2026-08-18] Migration-reset eligibility also blocks AGI `pending_signature` and `agi_submission_*` state plus every ROT/RUT payout request: both flows hand work to Skatteverket for external upload or BankID signing before Accounted can observe the filing outcome, so a missing receipt or locally generated/cancelled status cannot prove that the data is disposable.
|
||||
[2026-08-19] Keep reversal allocation metadata limited to failures before any reversal header exists: later cleanup preserves a cancelled header with the allocated voucher number, so documenting it as an unused voucher gap would be false.
|
||||
[2026-08-19] Bank reconciliation: ignored transactions are excluded from bank_transaction_total/difference (surfaced as separate count+sum) rather than keeping the old "what the bank moved" semantics: the ignore flag's dominant real-world use is feed duplicates (which never moved money and never get a ledger leg), so including them made is_reconciled unreachable after a correct dupe cleanup (observed: permanent 78 867 kr differens on a fully booked EF account). descriptionsBridge now strips ALL whitespace before the prefix compare (collapse-only still misses a dropped space); safe because char-filtering preserves prefix relations and the compare stays inside a (date,ore) bucket.
|
||||
[2026-08-19] Inline rättelse bank guard anchors to the linked bank amount, per account, not to the pre-state and not to the 19xx group net: a non-zero change on a 19xx/cash-ledger account is allowed iff the post-state net on that account equals the signed sum of the linked transactions resolved to it (once per transaction, split links by allocated_amount; NULL cash_account_id resolves to the primary cash account, then 1930). Per account rather than group so a wrong-bank-account booking (1930 vs 1940) stays a storno job: a group check would let the net drift between accounts and break per-account bank reconciliation. When no anchor resolves the old strict refusal stands. Reskontra sides (15xx/24xx) stay strictly net-preserving because their anchor is the payment row, not a bank amount.
|
||||
|
||||
@@ -18,8 +18,11 @@ import { getReconciliationStatus } from '@/lib/reconciliation/bank-reconciliatio
|
||||
// total_unmatched_amount, …) that the endpoint never returned; any client coded
|
||||
// against it read undefined for every field except difference.
|
||||
const StatusResponse = z.object({
|
||||
/** Sum of bank-feed transactions in the window (the bank side). */
|
||||
/** Sum of bank-feed transactions in the window (the bank side), excluding ignored rows. */
|
||||
bank_transaction_total: z.number(),
|
||||
/** Sum of ignored bank transactions in the window. Informational: not part of bank_transaction_total or difference. */
|
||||
ignored_transaction_total: z.number(),
|
||||
ignored_transaction_count: z.number().int(),
|
||||
/** Full ledger balance on the account incl. opening balance: matches the balance sheet. */
|
||||
gl_1930_balance: z.number(),
|
||||
/** Ledger movement excluding opening balance: what `difference` compares against. */
|
||||
@@ -50,11 +53,14 @@ registerEndpoint({
|
||||
'A non-zero difference is normal between sync runs (uncleared cheques, in-flight transfers). Investigate only if it persists across reconciliations.',
|
||||
'difference compares against gl_1930_period_movement (movement excl. opening balance), NOT gl_1930_balance. Do not display gl_1930_balance next to difference.',
|
||||
'is_reconciled means |difference| < 0.01 for the window, an aggregate check, not a per-transaction guarantee.',
|
||||
'Ignored transactions are excluded from bank_transaction_total and difference (they never get a ledger counterpart); their count and sum are reported separately.',
|
||||
],
|
||||
example: {
|
||||
response: {
|
||||
data: {
|
||||
bank_transaction_total: 48150,
|
||||
ignored_transaction_total: 0,
|
||||
ignored_transaction_count: 0,
|
||||
gl_1930_balance: 98150,
|
||||
gl_1930_period_movement: 48150,
|
||||
gl_1930_opening_balance: 50000,
|
||||
|
||||
@@ -113,7 +113,11 @@ const QUICK_BOOK_TEMPLATES: {
|
||||
// ============================================================
|
||||
|
||||
interface ReconciliationStatus {
|
||||
/** Bank-feed total EXCLUDING ignored rows: the reconciling bank side. */
|
||||
bank_transaction_total: number
|
||||
/** Sum of ignored rows in the window; informational, not in the difference. */
|
||||
ignored_transaction_total: number
|
||||
ignored_transaction_count: number
|
||||
/**
|
||||
* @deprecated Kept on the server response for back-compat. The UI no longer
|
||||
* reads it: `gl_1930_period_movement` is required.
|
||||
@@ -1061,6 +1065,14 @@ export function BankReconciliationView({ periodId, periodBounds, autoRun }: Bank
|
||||
{formatCurrency(status.difference)}
|
||||
</span>
|
||||
</div>
|
||||
{status.ignored_transaction_count > 0 && (
|
||||
<p className="pt-2 text-xs text-muted-foreground">
|
||||
{t('recon_ignored_note', {
|
||||
count: status.ignored_transaction_count,
|
||||
amount: formatCurrency(status.ignored_transaction_total, accountCurrency),
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
{status.gl_1930_opening_balance !== 0 && (
|
||||
<p className="pt-2 text-xs text-muted-foreground">
|
||||
Ingående balans (IB) på <AccountNumber number={accountNumber} />:{' '}
|
||||
|
||||
@@ -1336,6 +1336,86 @@ describe('getReconciliationStatus', () => {
|
||||
expect(status.gl_1930_period_movement).toBe(200)
|
||||
})
|
||||
|
||||
it('excludes ignored transactions from the bank total and difference, surfacing them separately', async () => {
|
||||
// The 2026-08-18 duplicate-cleanup shape: a PSD2 reconnect re-imported
|
||||
// history, the user booked one twin and IGNORED the other. The ignored
|
||||
// duplicate never gets a ledger counterpart, so counting it in the bank
|
||||
// total manufactured a permanent difference no amount of booking could
|
||||
// clear (observed live: a 78 867 kr differens over a fully booked account).
|
||||
const { supabase, enqueue } = createQueueMockSupabase()
|
||||
|
||||
// 1) transactions: booked original + its ignored duplicate + one real
|
||||
// unbooked deposit.
|
||||
enqueue({
|
||||
data: [
|
||||
{ amount: 8730, journal_entry_id: 'je-1', reconciliation_method: 'manual', is_ignored: false },
|
||||
{ amount: 8730, journal_entry_id: null, reconciliation_method: null, is_ignored: true },
|
||||
{ amount: 500, journal_entry_id: null, reconciliation_method: null, is_ignored: false },
|
||||
],
|
||||
})
|
||||
// 2) GL: only the booked original is on 1930.
|
||||
enqueue({ data: [{ id: 'je-1', status: 'posted', source_type: 'bank_import' }] })
|
||||
enqueue({ data: [{ debit_amount: 8730, credit_amount: 0, journal_entry_id: 'je-1' }] })
|
||||
// 3) RPC: no unlinked lines
|
||||
enqueue({ data: [] })
|
||||
|
||||
const status = await getReconciliationStatus(supabase as never, 'company-1')
|
||||
|
||||
expect(status.bank_transaction_total).toBe(9230) // 8730 + 500, NOT the ignored twin
|
||||
expect(status.ignored_transaction_count).toBe(1)
|
||||
expect(status.ignored_transaction_total).toBe(8730)
|
||||
expect(status.gl_1930_period_movement).toBe(8730)
|
||||
expect(status.difference).toBe(500) // only the real unbooked deposit remains
|
||||
expect(status.matched_count).toBe(1)
|
||||
expect(status.unmatched_transaction_count).toBe(1)
|
||||
expect(status.is_reconciled).toBe(false)
|
||||
})
|
||||
|
||||
it('reports is_reconciled=true once everything non-ignored is booked, despite ignored rows', async () => {
|
||||
const { supabase, enqueue } = createQueueMockSupabase()
|
||||
|
||||
enqueue({
|
||||
data: [
|
||||
{ amount: 1000, journal_entry_id: 'je-1', reconciliation_method: 'auto_exact', is_ignored: false },
|
||||
{ amount: 1000, journal_entry_id: null, reconciliation_method: null, is_ignored: true },
|
||||
],
|
||||
})
|
||||
enqueue({ data: [{ id: 'je-1', status: 'posted', source_type: 'bank_import' }] })
|
||||
enqueue({ data: [{ debit_amount: 1000, credit_amount: 0, journal_entry_id: 'je-1' }] })
|
||||
enqueue({ data: [] })
|
||||
|
||||
const status = await getReconciliationStatus(supabase as never, 'company-1')
|
||||
|
||||
expect(status.bank_transaction_total).toBe(1000)
|
||||
expect(status.ignored_transaction_total).toBe(1000)
|
||||
expect(status.difference).toBe(0)
|
||||
expect(status.unmatched_transaction_count).toBe(0)
|
||||
// Before the fix this was unreachable for any company with an ignored row:
|
||||
// the ignored amount sat in the bank total forever.
|
||||
expect(status.is_reconciled).toBe(true)
|
||||
})
|
||||
|
||||
it('does not count an ignored row that somehow retains a journal_entry_id as matched', async () => {
|
||||
const { supabase, enqueue } = createQueueMockSupabase()
|
||||
|
||||
enqueue({
|
||||
data: [
|
||||
{ amount: 700, journal_entry_id: 'je-x', reconciliation_method: 'manual', is_ignored: true },
|
||||
],
|
||||
})
|
||||
enqueue({ data: [] })
|
||||
enqueue({ data: [] })
|
||||
enqueue({ data: [] })
|
||||
|
||||
const status = await getReconciliationStatus(supabase as never, 'company-1')
|
||||
|
||||
// matched + unmatched partition the reconcilable (non-ignored) set.
|
||||
expect(status.matched_count).toBe(0)
|
||||
expect(status.unmatched_transaction_count).toBe(0)
|
||||
expect(status.ignored_transaction_count).toBe(1)
|
||||
expect(status.bank_transaction_total).toBe(0)
|
||||
})
|
||||
|
||||
it('reconciles a corrected bank receipt and keeps gl_1930_balance equal to the balance sheet', async () => {
|
||||
// A +25000 deposit was booked to the wrong counter-account, then corrected
|
||||
// via the storno flow: the original flips to 'reversed', a storno (credit
|
||||
|
||||
@@ -100,7 +100,20 @@ export interface ReconciliationStatus {
|
||||
* this is a no-op and every figure below is exactly what it always was.
|
||||
*/
|
||||
currency: string
|
||||
/**
|
||||
* Sum of the window's bank-feed transactions EXCLUDING ignored rows: the
|
||||
* bank side of the reconciliation. Ignored rows (feed duplicates from a
|
||||
* PSD2 reconnect, non-business noise) never get a ledger counterpart, so
|
||||
* counting them here manufactured a permanent unfixable difference; they are
|
||||
* surfaced separately below instead, mirroring how the opening balance is
|
||||
* excluded-but-shown.
|
||||
*/
|
||||
bank_transaction_total: number
|
||||
/** Sum of ignored bank transactions in the window. NOT part of
|
||||
* bank_transaction_total or difference; informational, like the IB. */
|
||||
ignored_transaction_total: number
|
||||
/** Number of ignored bank transactions in the window. */
|
||||
ignored_transaction_count: number
|
||||
/**
|
||||
* The real ledger balance on the bank account, incl. IB: computed from the
|
||||
* SAME `['posted','reversed']` lines the trial balance and balance sheet sum.
|
||||
@@ -655,10 +668,11 @@ export async function getReconciliationStatus(
|
||||
includeUnassigned: boolean = true,
|
||||
): Promise<ReconciliationStatus> {
|
||||
// Get all transactions in range, scoped to the selected cash account. Ignored
|
||||
// rows are pulled too so the totals card still reflects what the bank
|
||||
// actually moved, but they're excluded from the "unmatched" count below: the
|
||||
// user has explicitly said they don't want them surfacing as something to
|
||||
// reconcile. Scoping by cash account (not just currency) is what stops a
|
||||
// rows are pulled too, but only to be COUNTED AND SUMMED separately: they are
|
||||
// excluded from the bank total, the difference and the matched/unmatched
|
||||
// counts below, because the user has explicitly said they are not something
|
||||
// to reconcile (duplicates, non-business noise). Scoping by cash account
|
||||
// (not just currency) is what stops a
|
||||
// second same-currency account from inflating bankTotal here.
|
||||
// Paginated (fetchAllRows): PostgREST silently caps un-ranged selects at 1000
|
||||
// rows, which would undercount bank_transaction_total for a busy company and
|
||||
@@ -799,7 +813,7 @@ export async function getReconciliationStatus(
|
||||
onOrAfterFloor((tx as { date?: string | null }).date),
|
||||
)
|
||||
|
||||
// Bank side: every feed transaction in the (floored) window, full stop. We
|
||||
// Bank side: every NON-IGNORED feed transaction in the (floored) window. We
|
||||
// deliberately do NOT special-case rows linked to a reversed entry any more.
|
||||
// Because the GL side now counts the reversed original, its storno AND the
|
||||
// correction together (just like the balance sheet), a corrected bank line nets
|
||||
@@ -809,7 +823,20 @@ export async function getReconciliationStatus(
|
||||
// transactions.amount is denominated in transactions.currency, and
|
||||
// scopeTransactionsToAccount pinned that to `currency`, so this total is
|
||||
// already in the account's own currency: the unit lineAmount() resolves to.
|
||||
const bankTotal = countedTx.reduce((sum, tx) => sum + (Number(tx.amount) || 0), 0)
|
||||
//
|
||||
// Ignored rows are EXCLUDED from the total, exactly as they are excluded from
|
||||
// the unmatched count: ignoring is the sanctioned handling for feed
|
||||
// duplicates (a reconnect re-importing history) and non-business noise, and
|
||||
// by definition an ignored row will never get a ledger counterpart. Counting
|
||||
// it in the bank total manufactured a permanent difference the user could
|
||||
// never book away: after a correct duplicate cleanup the card showed a
|
||||
// six-figure differens over a fully booked account, and is_reconciled was
|
||||
// unreachable forever. They are surfaced separately (count + sum) instead,
|
||||
// the same pattern as the opening balance, so nothing is silently hidden.
|
||||
const reconcilableTx = countedTx.filter((tx) => tx.is_ignored !== true)
|
||||
const ignoredTx = countedTx.filter((tx) => tx.is_ignored === true)
|
||||
const bankTotal = reconcilableTx.reduce((sum, tx) => sum + (Number(tx.amount) || 0), 0)
|
||||
const ignoredTotal = ignoredTx.reduce((sum, tx) => sum + (Number(tx.amount) || 0), 0)
|
||||
|
||||
// Ledger lines the account's currency cannot express: a foreign account whose
|
||||
// lines hold only SEK figures with no per-row rate (SIE imports, pre-FX
|
||||
@@ -844,10 +871,13 @@ export async function getReconciliationStatus(
|
||||
// a bank-feed counterpart, so it stays in.
|
||||
const glPeriodMovement = glBalance - glOpeningBalance
|
||||
|
||||
const matchedCount = countedTx.filter((tx) => tx.journal_entry_id !== null).length
|
||||
// Matched/unmatched partition the RECONCILABLE (non-ignored) set, so
|
||||
// matched_count + unmatched_transaction_count always equals the number of
|
||||
// rows behind bank_transaction_total.
|
||||
const matchedCount = reconcilableTx.filter((tx) => tx.journal_entry_id !== null).length
|
||||
|
||||
const unmatchedTransactionCount = countedTx.filter(
|
||||
(tx) => tx.journal_entry_id === null && tx.is_ignored !== true
|
||||
const unmatchedTransactionCount = reconcilableTx.filter(
|
||||
(tx) => tx.journal_entry_id === null
|
||||
).length
|
||||
|
||||
// Unmatched GL lines count (RPC excludes opening_balance, storno and correction
|
||||
@@ -866,6 +896,8 @@ export async function getReconciliationStatus(
|
||||
return {
|
||||
currency,
|
||||
bank_transaction_total: Math.round(bankTotal * 100) / 100,
|
||||
ignored_transaction_total: Math.round(ignoredTotal * 100) / 100,
|
||||
ignored_transaction_count: ignoredTx.length,
|
||||
gl_1930_balance: Math.round(glBalance * 100) / 100,
|
||||
gl_1930_period_movement: Math.round(glPeriodMovement * 100) / 100,
|
||||
gl_1930_opening_balance: Math.round(glOpeningBalance * 100) / 100,
|
||||
|
||||
@@ -134,6 +134,39 @@ describe('descriptionsBridge', () => {
|
||||
expect(descriptionsBridge(' Mataffär Solna ', 'mataffär solna')).toBe(true)
|
||||
})
|
||||
|
||||
it('bridges whitespace-only rendering drift, including a DROPPED space (2026-08-18 reconnect incident)', () => {
|
||||
// The same bank re-rendered the same transactions with different spacing
|
||||
// after a PSD2 reconnect; every pair below is a real observed twin shape.
|
||||
// CRLF vs space:
|
||||
expect(
|
||||
descriptionsBridge(
|
||||
'2025DEC58 BRANDHEROES\r\nURSPRUNGLIGT BELOPP M M: EUR',
|
||||
'2025DEC58 BRANDHEROES URSPRUNGLIGT BELOPP M M: EUR'
|
||||
)
|
||||
).toBe(true)
|
||||
// Dropped space (collapse-only normalization would still miss this):
|
||||
expect(
|
||||
descriptionsBridge(
|
||||
'005 BaBylissURSPRUNGLIGT BELOPP M M: SEK',
|
||||
'005 BaByliss URSPRUNGLIGT BELOPP M M: SEK'
|
||||
)
|
||||
).toBe(true)
|
||||
// Trailing padding + reference tail growth still bridges via prefix rule:
|
||||
expect(
|
||||
descriptionsBridge(
|
||||
'BRANDHEROES APSFAK 008URSPRUNGLIGT BELOPP M M: EUR',
|
||||
'BRANDHEROES APSFAK 008 URSPRUNGLIGT BELOPP M M: EUR 2162943703022614'
|
||||
)
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('whitespace stripping does not bridge genuinely distinct references', () => {
|
||||
expect(descriptionsBridge('REF-AAAA 1111', 'REF-BBBB 2222')).toBe(false)
|
||||
// A whitespace-only description is still treated as blank (no wildcard).
|
||||
expect(descriptionsBridge(' \r\n ', 'anything')).toBe(false)
|
||||
expect(descriptionsBridge(' \r\n ', ' ')).toBe(true)
|
||||
})
|
||||
|
||||
it('does NOT bridge genuinely distinct descriptions sharing a date+amount', () => {
|
||||
// Distinct reference codes on same-day same-amount rows (e.g. verification
|
||||
// micro-deposits) must NOT collapse: each is a real transaction.
|
||||
|
||||
@@ -139,7 +139,9 @@ export function contentBucketKey(date: string, amount: number | string): string
|
||||
* ("TIC" → "TIC BG 0000005786439 Bg-bet. via internet", "UTBETALNING" →
|
||||
* "UTBETALNING Insättning"), so prefix-containment bridges the two where a
|
||||
* fixed-length prefix *equality* check (the pre-June-2026 scheme) missed and
|
||||
* re-imported. A blank description carries no signal, so it never bridges a
|
||||
* re-imported. Whitespace is not part of the signal: both sides are compared
|
||||
* with ALL whitespace stripped, because banks reformat spacing between API
|
||||
* sessions (see the inline comment). A blank description carries no signal, so it never bridges a
|
||||
* *described* row: otherwise an empty title would wildcard-match any
|
||||
* same-(date,öre) transaction and could silently consume a real one; only two
|
||||
* blanks bridge each other (date+öre identity). In practice every caller
|
||||
@@ -158,8 +160,19 @@ export function descriptionsBridge(
|
||||
a: string | null | undefined,
|
||||
b: string | null | undefined
|
||||
): boolean {
|
||||
const x = (a ?? '').toLowerCase().trim()
|
||||
const y = (b ?? '').toLowerCase().trim()
|
||||
// Whitespace is stripped ENTIRELY (not collapsed) before comparing: the same
|
||||
// bank renders the same transaction with drifting whitespace between API
|
||||
// sessions ("BaBylissURSPRUNGLIGT" vs "BaByliss URSPRUNGLIGT", CRLF vs
|
||||
// space), and a collapse-only normalization still misses the dropped-space
|
||||
// form. Observed in the 2026-08-18 reconnect incident: five historical rows
|
||||
// re-imported as twins whose descriptions differed only in whitespace, and
|
||||
// every dedup layer missed them. Stripping is safe for the title-prefix
|
||||
// bridge too: char-filtering is concatenation-homomorphic, so every existing
|
||||
// prefix relation is preserved (it can only ADD bridges, never remove one).
|
||||
// The compare stays confined to a (date, öre) bucket, so the widened match
|
||||
// can only collapse same-day same-amount rows.
|
||||
const x = (a ?? '').toLowerCase().replace(/\s+/g, '')
|
||||
const y = (b ?? '').toLowerCase().replace(/\s+/g, '')
|
||||
// A blank never wildcards a described row; only two blanks bridge each other.
|
||||
if (x === '' || y === '') return x === y
|
||||
return x.startsWith(y) || y.startsWith(x)
|
||||
|
||||
@@ -6474,6 +6474,7 @@
|
||||
"help_bank_reconciliation_ib": "Is a manually booked or imported voucher actually an opening balance? Mark it as IB and it is excluded from the reconciliation and shown separately.",
|
||||
"help_bank_reconciliation_ignored": "Ignored transactions are hidden from the reconciliation without being booked. They do not affect the balance and can be restored at any time.",
|
||||
"recon_unmatched_attn": "{count, plural, one {1 unmatched transaction: Preview finds automatic matches.} other {# unmatched transactions: Preview finds automatic matches.}}",
|
||||
"recon_ignored_note": "{count, plural, one {1 ignored transaction ({amount}) is excluded from the reconciliation. You can restore it under Ignored transactions below.} other {# ignored transactions ({amount}) are excluded from the reconciliation. You can restore them under Ignored transactions below.}}",
|
||||
"recon_apply_strong": "{count, plural, one {Match 1 strong match} other {Match # strong matches}}",
|
||||
"switch_report": "Switch report",
|
||||
"calendar_badge": "Calendar",
|
||||
|
||||
@@ -6474,6 +6474,7 @@
|
||||
"help_bank_reconciliation_ib": "Är en manuellt bokförd eller importerad verifikation egentligen en ingående balans? Märk den som IB så räknas den inte med i avstämningen utan visas separat.",
|
||||
"help_bank_reconciliation_ignored": "Ignorerade transaktioner döljs från avstämningen utan att bokföras. De påverkar inte saldot och kan återställas när som helst.",
|
||||
"recon_unmatched_attn": "{count, plural, one {1 omatchad transaktion: Förhandsgranska hittar automatiska träffar.} other {# omatchade transaktioner: Förhandsgranska hittar automatiska träffar.}}",
|
||||
"recon_ignored_note": "{count, plural, one {1 ignorerad transaktion ({amount}) räknas inte med i avstämningen. Du kan återställa den under Ignorerade transaktioner nedan.} other {# ignorerade transaktioner ({amount}) räknas inte med i avstämningen. Du kan återställa dem under Ignorerade transaktioner nedan.}}",
|
||||
"recon_apply_strong": "{count, plural, one {Matcha 1 stark träff} other {Matcha # starka träffar}}",
|
||||
"switch_report": "Byt rapport",
|
||||
"calendar_badge": "Kalender",
|
||||
|
||||
@@ -144,6 +144,7 @@ Returns matched / unmatched counts and the balance delta between the bank ledger
|
||||
- A non-zero difference is normal between sync runs (uncleared cheques, in-flight transfers). Investigate only if it persists across reconciliations.
|
||||
- difference compares against gl_1930_period_movement (movement excl. opening balance), NOT gl_1930_balance. Do not display gl_1930_balance next to difference.
|
||||
- is_reconciled means |difference| < 0.01 for the window, an aggregate check, not a per-transaction guarantee.
|
||||
- Ignored transactions are excluded from bank_transaction_total and difference (they never get a ledger counterpart); their count and sum are reported separately.
|
||||
|
||||
| Parameter | In | Type | Required | Notes |
|
||||
|---|---|---|---|---|
|
||||
@@ -154,6 +155,8 @@ Response `200`:
|
||||
{
|
||||
data: {
|
||||
bank_transaction_total: number,
|
||||
ignored_transaction_total: number,
|
||||
ignored_transaction_count: number,
|
||||
gl_1930_balance: number,
|
||||
gl_1930_period_movement: number,
|
||||
gl_1930_opening_balance: number,
|
||||
|
||||
Reference in New Issue
Block a user