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:
Jakob Wennberg
2026-06-03 10:52:01 +02:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 331ae11867
commit c74b19df1b
183 changed files with 19621 additions and 4175 deletions
@@ -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)
})
+1 -1
View File
@@ -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.
+28
View File
@@ -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,
},
}
}
+30
View File
@@ -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,
},
}
}