ec27228a8e
Em dashes (—) and en dashes (–) had spread across comments, docs, tests, and a few UI strings, reading as AI-generated boilerplate rather than house style. Replaced each with punctuation matching its context: colon for explanatory clauses, comma for asides, plain hyphen for numeric/legal ranges (e.g. "21-23§"), "to"/"till" for date ranges, parentheses for paired-dash asides. messages/en.json and messages/sv.json were fixed by hand together to keep sv/en in sync. Left untouched where the dash is the functional subject rather than decorative punctuation: date-range-parser.ts's separator regex, charset-repair.ts's CP1252 byte-mapping table (and its test), the SIE encoding mojibake docs, generic-csv.ts's minus-sign normalizer, the agent system-prompt files that already instruct against em dashes, and a golden iXBRL test fixture compared byte-for-byte. Also fixes two bugs surfaced along the way: an off-by-one in ApiKeysPanel's scope-label split (a leftover from an earlier partial pass), and a charset-repair test that had lost the literal en-dash it exists to verify. Regenerated the agent atom seed migration (skills:generate) since 27 SKILL.md files changed. Added a CLAUDE.md rule against em/en dashes, with an explicit carve-out for the functional-dash cases above. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
96 lines
3.6 KiB
TypeScript
96 lines
3.6 KiB
TypeScript
import type { SupabaseClient } from '@supabase/supabase-js'
|
|
import type { MappingResult } from '@/types'
|
|
import { getBASReference } from '@/lib/bookkeeping/bas-reference'
|
|
|
|
/**
|
|
* Return the subset of `accountNumbers` that are NOT present-and-active in the
|
|
* given company's chart_of_accounts. Mirrors the engine's resolveAccountIds
|
|
* (lib/bookkeeping/engine.ts) so a pre-validation in API routes catches the
|
|
* same condition (AccountsNotInChartError) before any DB writes happen.
|
|
*
|
|
* Empty/duplicate inputs are normalised; preserves first-seen order in output.
|
|
* On Supabase error: bubbles up. A chart-of-accounts read failure is real
|
|
* infrastructure trouble and should surface as 500 rather than be silently
|
|
* masked as "account missing".
|
|
*/
|
|
export async function findMissingActiveAccounts(
|
|
supabase: SupabaseClient,
|
|
companyId: string,
|
|
accountNumbers: readonly string[],
|
|
): Promise<string[]> {
|
|
const seen = new Set<string>()
|
|
const unique: string[] = []
|
|
for (const num of accountNumbers) {
|
|
if (!num) continue
|
|
if (seen.has(num)) continue
|
|
seen.add(num)
|
|
unique.push(num)
|
|
}
|
|
if (unique.length === 0) return []
|
|
|
|
const { data, error } = await supabase
|
|
.from('chart_of_accounts')
|
|
.select('account_number')
|
|
.eq('company_id', companyId)
|
|
.eq('is_active', true)
|
|
.in('account_number', unique)
|
|
|
|
if (error) throw error
|
|
|
|
const present = new Set<string>((data ?? []).map((r) => r.account_number as string))
|
|
return unique.filter((n) => !present.has(n))
|
|
}
|
|
|
|
/**
|
|
* Return the subset of `accountNumbers` that the engine cannot resolve even
|
|
* after its on-demand backfill (lib/bookkeeping/account-backfill.ts):
|
|
*
|
|
* - numbers with no BAS 2026 reference (typos, non-standard accounts), and
|
|
* - accounts that exist in the chart but are deactivated (the backfill never
|
|
* resurrects a deliberate deactivation).
|
|
*
|
|
* An account that is simply absent from the chart but exists in BAS is NOT
|
|
* returned: createDraftEntry seeds it automatically, so pre-validation in a
|
|
* route must not 400 on it. Read-only on purpose: dry-run/preview paths use
|
|
* the same check without side effects. Preserves first-seen order.
|
|
*/
|
|
export async function findUnresolvableAccounts(
|
|
supabase: SupabaseClient,
|
|
companyId: string,
|
|
accountNumbers: readonly string[],
|
|
): Promise<string[]> {
|
|
const missing = await findMissingActiveAccounts(supabase, companyId, accountNumbers)
|
|
if (missing.length === 0) return []
|
|
|
|
const basSeedable = missing.filter((num) => Boolean(getBASReference(num)))
|
|
if (basSeedable.length === 0) return missing
|
|
|
|
// A row that exists (but is inactive) blocks the backfill; a BAS number
|
|
// with no row at all will be seeded by the engine.
|
|
const { data, error } = await supabase
|
|
.from('chart_of_accounts')
|
|
.select('account_number')
|
|
.eq('company_id', companyId)
|
|
.in('account_number', basSeedable)
|
|
|
|
if (error) throw error
|
|
|
|
const existsInactive = new Set<string>((data ?? []).map((r) => r.account_number as string))
|
|
return missing.filter((num) => !getBASReference(num) || existsInactive.has(num))
|
|
}
|
|
|
|
/**
|
|
* Extract every chart account number a MappingResult will post to: the headline
|
|
* debit/credit plus every account_number in vat_lines. Returns the raw list
|
|
* (duplicates intact); pass through findMissingActiveAccounts to dedupe.
|
|
*/
|
|
export function collectMappingResultAccounts(mr: MappingResult): string[] {
|
|
const out: string[] = []
|
|
if (mr.debit_account) out.push(mr.debit_account)
|
|
if (mr.credit_account) out.push(mr.credit_account)
|
|
for (const line of mr.vat_lines ?? []) {
|
|
if (line.account_number) out.push(line.account_number)
|
|
}
|
|
return out
|
|
}
|