Accounted rebrand + swarm-skill cleanup + bank-reconciliation fixes (#643)
* feat(reconciliation): close the bank-feed loop on voucher links and re-tag mis-typed opening balances
Two related fixes to bank reconciliation correctness:
1. Auto-reconcile on voucher link. Linking an invoice or supplier invoice to
an existing voucher previously advanced only the invoice — the bank
transaction that paid it kept sitting in the Transactions inbox with a null
journal_entry_id. linkInvoiceToVoucher / linkSupplierInvoiceToVoucher now
call autoReconcileTransactionForLinkedVoucher (lib/reconciliation), which
links the bank transaction to the same verifikat when exactly one unbooked
line matches it. Best-effort and post-commit: a failure here never fails the
link. The result surfaces reconciledTransactionId; the inbox row leaves the
list and the UI shows link_success_tx_reconciled.
2. Re-tag mis-typed opening balances. getReconciliationStatus and the GL-line
matching RPCs identify a cash account's ingående balans solely by
journal_entries.source_type='opening_balance'. Companies migrated from other
systems often booked the bank IB as an ordinary voucher (source_type
'import' or 'manual'), so it was never excluded and surfaced as a phantom
reconciliation difference equal to the opening balance. Adds:
- migration mark_entry_as_opening_balance: a GUC-gated carve-out in the
immutability trigger plus a SECURITY DEFINER RPC that validates the entry
(balance-sheet lines only, dated on a fiscal-period boundary), flips the
source_type, and writes an audit row — no blanket data sweep.
- POST /api/reconciliation/bank/mark-opening-balance + MarkOpeningBalanceSchema.
- BankReconciliationView action to trigger it from the IB diff.
The gnubok_create_voucher executor now accepts a typed is_opening_balance flag
and derives source_type='opening_balance' only after validating class 1/2 lines
on the period start, so new IBs land correctly typed.
Covered by lib/reconciliation auto-reconcile tests, voucher-executors tests,
and a mark-entry-as-opening-balance pg-real test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: rebrand gnubok → Accounted and prune swarm agent skills
Product rebrand and skills housekeeping. No runtime behaviour change.
Rebrand: replace user-visible "gnubok" with "Accounted" across docs, READMEs,
in-code comments, doc-site content, MCP skill/resource prose, and the
gnubok-mcp package description. The MCP resource URI scheme is moved gnubok://
→ Accounted:// consistently across resource registrations, the event-type
comment, and the resource/skill tests. Deliberately preserved as stable
identifiers (NOT rebranded): the gnubok-company-id cookie, gnubok_sk_ / gnubok_inv_
token prefixes, the gnubok-mcp npm bridge name, and the AGI <gem:Programnamn>
value (kept 'gnubok' per its source comment — it is the software identifier sent
to Skatteverket and must not churn across visual rebrands).
Skills: remove the 27 swarm-* agent SKILL.md atoms (no longer used; already
absent from the agent_atom_registry in prod), refresh the remaining skill docs,
add the .claude/rules/ path-scoped rule set, and regenerate the
seed_agent_atom_bodies migration + .skill-body-manifest.json via
`npm run skills:generate` so the DB-backed skill bodies match the trimmed set.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
331ae11867
commit
c74b19df1b
@@ -60,21 +60,21 @@ describe('isWcagAACompliant', () => {
|
||||
expect(isWcagAACompliant('#ffff00', '#ffffff')).toBe(false)
|
||||
})
|
||||
|
||||
// gnubok brand semantic colors. All three are used as DATA-ONLY indicators
|
||||
// Accounted brand semantic colors. All three are used as DATA-ONLY indicators
|
||||
// (charts, positive/negative deltas), never as chrome text on white. These
|
||||
// tests document where they sit relative to AA — terracotta/destructive is
|
||||
// the only one that comfortably passes AA on white.
|
||||
it('gnubok terracotta (#c2410c equivalent dark red) passes AA on white', () => {
|
||||
it('Accounted terracotta (#c2410c equivalent dark red) passes AA on white', () => {
|
||||
// 5.91:1 — passes AA for normal text.
|
||||
expect(isWcagAACompliant('#c2410c', '#ffffff')).toBe(true)
|
||||
})
|
||||
|
||||
it('gnubok sage (#84a98c lighter green) fails AA on white', () => {
|
||||
it('Accounted sage (#84a98c lighter green) fails AA on white', () => {
|
||||
// ~2.4:1 — fails AA, as expected for a soft pastel sage.
|
||||
expect(isWcagAACompliant('#84a98c', '#ffffff')).toBe(false)
|
||||
})
|
||||
|
||||
it('gnubok ochre (#d4a373 warm yellow) fails AA on white', () => {
|
||||
it('Accounted ochre (#d4a373 warm yellow) fails AA on white', () => {
|
||||
// ~2.3:1 — fails AA, as expected for a warm soft ochre.
|
||||
expect(isWcagAACompliant('#d4a373', '#ffffff')).toBe(false)
|
||||
})
|
||||
|
||||
@@ -54,7 +54,7 @@ const LATE_PAYMENT_PREMIUM = 0.08 // 8 procentenheter per Räntelagen §6
|
||||
*
|
||||
* If the requested date is before the earliest entry in the table we fall
|
||||
* back to the earliest entry (this is a defensive measure — should never
|
||||
* happen in practice since gnubok was launched after 2022).
|
||||
* happen in practice since Accounted was launched after 2022).
|
||||
*/
|
||||
export function getReferensrantaAt(date: string): number {
|
||||
// Walk newest-first so the first match wins.
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
amountsMatchFuzzy,
|
||||
customerNameMatches,
|
||||
} from './invoice-matching'
|
||||
import { autoReconcileTransactionForLinkedVoucher } from '@/lib/reconciliation/bank-reconciliation'
|
||||
import type { SupplierInvoice, Supplier } from '@/types'
|
||||
|
||||
const log = createLogger('supplier-voucher-matching')
|
||||
@@ -480,6 +481,9 @@ export interface LinkSupplierInvoiceToVoucherResult {
|
||||
remainingAmount: number
|
||||
paymentAmount: number
|
||||
journalEntryId: string
|
||||
/** Bank transaction auto-reconciled to the linked voucher, if exactly one
|
||||
* unbooked line matched it; null when nothing was safely linkable. */
|
||||
reconciledTransactionId: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -594,6 +598,29 @@ export async function linkSupplierInvoiceToVoucher(
|
||||
}
|
||||
}
|
||||
|
||||
// Close the loop on the bank feed: the link above only advanced the supplier
|
||||
// invoice, leaving the bank transaction that paid it in the Transactions
|
||||
// inbox. Reconcile it to the same verifikat when unambiguous. Best-effort —
|
||||
// the link RPC has already committed.
|
||||
let reconciledTransactionId: string | null = null
|
||||
try {
|
||||
const recon = await autoReconcileTransactionForLinkedVoucher(
|
||||
supabase,
|
||||
companyId,
|
||||
userId,
|
||||
params.journalEntryId,
|
||||
{ supplierInvoiceId: params.supplierInvoiceId },
|
||||
)
|
||||
reconciledTransactionId = recon?.linkedTransactionId ?? null
|
||||
} catch (err) {
|
||||
log.warn('auto-reconcile of bank transaction after supplier voucher link failed (non-blocking)', {
|
||||
companyId,
|
||||
supplierInvoiceId: params.supplierInvoiceId,
|
||||
journalEntryId: params.journalEntryId,
|
||||
reason: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
result: {
|
||||
@@ -603,6 +630,7 @@ export async function linkSupplierInvoiceToVoucher(
|
||||
remainingAmount: result.remaining_amount,
|
||||
paymentAmount: result.payment_amount,
|
||||
journalEntryId: result.journal_entry_id,
|
||||
reconciledTransactionId,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
amountsMatchFuzzy,
|
||||
customerNameMatches,
|
||||
} from './invoice-matching'
|
||||
import { autoReconcileTransactionForLinkedVoucher } from '@/lib/reconciliation/bank-reconciliation'
|
||||
import type { Invoice, Customer } from '@/types'
|
||||
|
||||
const log = createLogger('voucher-matching')
|
||||
@@ -460,6 +461,10 @@ export interface LinkInvoiceToVoucherResult {
|
||||
remainingAmount: number
|
||||
paymentAmount: number
|
||||
journalEntryId: string
|
||||
/** Bank transaction auto-reconciled to the linked voucher, if exactly one
|
||||
* unbooked line matched it; null when nothing was safely linkable. Lets the
|
||||
* inbox row leave the Transactions list — the gap this whole flow fixes. */
|
||||
reconciledTransactionId: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -602,6 +607,30 @@ export async function linkInvoiceToVoucher(
|
||||
/* non-critical */
|
||||
}
|
||||
|
||||
// Close the loop on the bank feed: the invoice→voucher link above only
|
||||
// advanced the invoice, so the bank transaction that paid it kept sitting in
|
||||
// the Transactions inbox (journal_entry_id still null). Reconcile it to the
|
||||
// same verifikat when it can be done unambiguously. Best-effort — the invoice
|
||||
// link has already committed, so a failure here must not fail the whole call.
|
||||
let reconciledTransactionId: string | null = null
|
||||
try {
|
||||
const recon = await autoReconcileTransactionForLinkedVoucher(
|
||||
supabase,
|
||||
companyId,
|
||||
userId,
|
||||
params.journalEntryId,
|
||||
{ invoiceId: params.invoiceId },
|
||||
)
|
||||
reconciledTransactionId = recon?.linkedTransactionId ?? null
|
||||
} catch (err) {
|
||||
log.warn('auto-reconcile of bank transaction after voucher link failed (non-blocking)', {
|
||||
companyId,
|
||||
invoiceId: params.invoiceId,
|
||||
journalEntryId: params.journalEntryId,
|
||||
reason: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
result: {
|
||||
@@ -611,6 +640,7 @@ export async function linkInvoiceToVoucher(
|
||||
remainingAmount: newRemaining,
|
||||
paymentAmount: validation.paymentAmount,
|
||||
journalEntryId: params.journalEntryId,
|
||||
reconciledTransactionId,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user