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>
179 lines
6.9 KiB
TypeScript
179 lines
6.9 KiB
TypeScript
/**
|
|
* POST /api/dimensions/import-existing: one-shot scan that backfills the
|
|
* dimension_values registry from codes already present on journal lines.
|
|
*
|
|
* The settings toggle-on flow runs this ("Importera befintliga koder",
|
|
* dev_docs/dimensions_implementation_plan.md §4): every {sie_dim_no: code}
|
|
* entry found in journal_entry_lines.dimensions that lacks a registry row gets
|
|
* one created with is_active = false and name = code: referential validity
|
|
* holds retroactively without polluting pickers. Dimensions missing from the
|
|
* registry entirely (e.g. a custom dim number written via the v1 API) are
|
|
* created too, so no line code is skipped. Idempotent: re-running creates
|
|
* nothing new.
|
|
*
|
|
* Response: 200 { created: number } (count of dimension_values created).
|
|
*/
|
|
import { NextResponse } from 'next/server'
|
|
import { ensureInitialized } from '@/lib/init'
|
|
import { withRouteContext } from '@/lib/api/with-route-context'
|
|
import { fetchAllRows } from '@/lib/supabase/fetch-all'
|
|
import { errorResponse, errorResponseFromCode } from '@/lib/errors/get-structured-error'
|
|
|
|
ensureInitialized()
|
|
|
|
interface LineRow {
|
|
id: string
|
|
dimensions: Record<string, string> | null
|
|
}
|
|
|
|
export const POST = withRouteContext(
|
|
'dimension.import_existing',
|
|
async (_request, ctx) => {
|
|
const { supabase, companyId, log, requestId } = ctx
|
|
|
|
const { error: ensureError } = await supabase.rpc('ensure_company_dimensions', {
|
|
p_company_id: companyId,
|
|
})
|
|
if (ensureError) {
|
|
log.error('ensure_company_dimensions failed', ensureError)
|
|
return errorResponse(ensureError, log, { requestId })
|
|
}
|
|
|
|
try {
|
|
// 1. Collect every {dim_no: code} pair used on this company's lines.
|
|
// Inner-join scoping: journal_entry_lines has no company_id column.
|
|
const lines = await fetchAllRows<LineRow>(({ from, to }) =>
|
|
supabase
|
|
.from('journal_entry_lines')
|
|
.select('id, dimensions, journal_entries!inner(company_id)')
|
|
.eq('journal_entries.company_id', companyId)
|
|
.neq('dimensions', '{}')
|
|
.order('id', { ascending: true })
|
|
.range(from, to),
|
|
)
|
|
|
|
const codesByDimNo = new Map<number, Set<string>>()
|
|
for (const line of lines) {
|
|
if (!line.dimensions) continue
|
|
for (const [key, code] of Object.entries(line.dimensions)) {
|
|
const dimNo = Number(key)
|
|
if (!Number.isInteger(dimNo) || dimNo < 1) continue
|
|
if (typeof code !== 'string' || code.length === 0) continue
|
|
// Sanitize exactly like the PR1 substrate backfill
|
|
// (left(regexp_replace(code, '["{}]', '', 'g'), 40)) so candidates
|
|
// satisfy the dimension_values code CHECK; codes that sanitize to
|
|
// empty are dropped, and the Set de-duplicates post-sanitization.
|
|
const sanitized = code.replace(/["{}]/g, '').slice(0, 40)
|
|
if (sanitized.length === 0) continue
|
|
const set = codesByDimNo.get(dimNo) ?? new Set<string>()
|
|
set.add(sanitized)
|
|
codesByDimNo.set(dimNo, set)
|
|
}
|
|
}
|
|
|
|
if (codesByDimNo.size === 0) {
|
|
return NextResponse.json({ created: 0 })
|
|
}
|
|
|
|
// 2. Registry state: dim_no → dimension_id, plus existing value codes.
|
|
const { data: dims, error: dimsError } = await supabase
|
|
.from('dimensions')
|
|
.select('id, sie_dim_no')
|
|
.eq('company_id', companyId)
|
|
if (dimsError) throw dimsError
|
|
|
|
const dimIdByNo = new Map<number, string>(
|
|
((dims ?? []) as { id: string; sie_dim_no: number }[]).map((d) => [d.sie_dim_no, d.id]),
|
|
)
|
|
|
|
// 3. Create registry rows for dim numbers seen on lines but missing from
|
|
// the registry (written via raw v1 JSON / MCP before the register
|
|
// existed). is_system=false; the user can rename them afterwards.
|
|
const missingDimNos = [...codesByDimNo.keys()].filter((n) => !dimIdByNo.has(n))
|
|
if (missingDimNos.length > 0) {
|
|
// ignoreDuplicates: a concurrent creator (double-click, MCP) must not
|
|
// abort the whole batch with a 23505: the survivor's row wins.
|
|
const { data: createdDims, error: createDimsError } = await supabase
|
|
.from('dimensions')
|
|
.upsert(
|
|
missingDimNos.map((n) => ({
|
|
company_id: companyId,
|
|
sie_dim_no: n,
|
|
name: `Dimension ${n}`,
|
|
is_system: false,
|
|
})),
|
|
{ onConflict: 'company_id,sie_dim_no', ignoreDuplicates: true },
|
|
)
|
|
.select('id, sie_dim_no')
|
|
if (createDimsError) throw createDimsError
|
|
for (const d of (createdDims ?? []) as { id: string; sie_dim_no: number }[]) {
|
|
dimIdByNo.set(d.sie_dim_no, d.id)
|
|
}
|
|
}
|
|
|
|
const { data: existingValues, error: valuesError } = await supabase
|
|
.from('dimension_values')
|
|
.select('dimension_id, code')
|
|
.eq('company_id', companyId)
|
|
if (valuesError) throw valuesError
|
|
|
|
const existing = new Set(
|
|
((existingValues ?? []) as { dimension_id: string; code: string }[]).map(
|
|
(v) => `${v.dimension_id} |