Files
accounted/lib/company/tic-refresh.ts
T
Jakob Wennberg ec27228a8e style: remove em/en dashes repo-wide, add CLAUDE.md rule against them (#890)
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>
2026-07-04 15:58:06 +02:00

89 lines
3.6 KiB
TypeScript

'use server'
import { cookies, headers } from 'next/headers'
import { revalidatePath } from 'next/cache'
import { createClient } from '@/lib/supabase/server'
import { normalizeOrgNumber } from '@/lib/company-lookup/normalize-org-number'
import { ensureTicSnapshot } from '@/lib/agent/composer/tic-fetch'
export interface RefreshCompanyProfileResult {
ok?: true
snapshot?: Record<string, unknown> | null
fetchedAt?: string
// Error *codes*, translated by the caller (same pattern as company/actions.ts):
// unauthorized | org_number_invalid | persist_failed | not_found
error?: string
}
/**
* Fetch Bolagsuppgifter on demand from the settings → Företag panel.
*
* The panel normally shows the cached `companies.tic_snapshot`. This action
* lets the user (re)fetch it live by submitting an org number / personnummer:
* the path that recovers a company whose cached snapshot is missing or wrong
* (e.g. an enskild firma whose 10-digit personnummer previously fuzzy-matched
* the wrong entity; `searchCompanyByOrgNumber` now expands it to the 12-digit
* form so Lens resolves it exactly).
*
* We persist the (normalized) number and clear `tic_snapshot_fetched_at` to
* force `ensureTicSnapshot` past its 7-day cache, then let it do the live
* /profile fetch + write. All writes are RLS-scoped to the caller's company.
*/
export async function refreshCompanyProfileAction(
companyId: string,
orgNumberRaw: string,
): Promise<RefreshCompanyProfileResult> {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return { error: 'unauthorized' }
// Refuse malformed input at the boundary rather than storing a value that
// would later break SIE/SRU exports (same rule as createCompanyFromOnboarding).
const cleaned = normalizeOrgNumber(orgNumberRaw)
if (!cleaned) return { error: 'org_number_invalid' }
// Persist the (possibly corrected) number and force staleness so
// ensureTicSnapshot re-fetches instead of returning the poisoned cache.
const { error: updateError } = await supabase
.from('companies')
.update({ org_number: cleaned, tic_snapshot_fetched_at: null })
.eq('id', companyId)
if (updateError) return { error: 'persist_failed' }
// Keep the settings form (which reads company_settings.org_number) in sync,
// best-effort; the TIC fetch reads companies.org_number, updated above.
await supabase
.from('company_settings')
.update({ org_number: cleaned })
.eq('company_id', companyId)
// Self-fetch needs the caller's session cookie and the current origin so it
// reaches this same instance (dev / preview / prod); see ensureTicSnapshot.
const cookieStore = await cookies()
const cookieHeader = cookieStore.getAll().map((c) => `${c.name}=${c.value}`).join('; ')
const hdrs = await headers()
const host = hdrs.get('host')
const proto = hdrs.get('x-forwarded-proto') ?? 'https'
const origin = host ? `${proto}://${host}` : undefined
const { snapshot, source } = await ensureTicSnapshot({
supabase,
companyId,
cookieHeader,
origin,
// The user is watching a spinner; give the ~7-13 call Lens fan-out room to
// finish (the 5s default aborted every fetch during the May quota incident).
timeoutMs: 10_000,
})
// 'fetched' = a fresh live fetch was persisted. 'fallback' = TIC returned
// nothing / errored: surface it and leave the existing snapshot untouched
// rather than blanking a good panel on a transient outage.
if (source !== 'fetched' || !snapshot) {
return { error: 'not_found' }
}
revalidatePath('/settings')
return { ok: true, snapshot, fetchedAt: new Date().toISOString() }
}