cd64c0e3fb
* feat(skatteverket): production-ready momsdeklaration submission
Brings the Skatteverket extension up to a state where it can ship moms
declaration submission to Vercel production. Verified end-to-end against
SKV's Komplett testtjänst — all 8 momsdeklaration operations tested
(kontrollera, spara/hämta/radera utkast, lås/lås upp, hämta inlämnade,
hämta beslutade) plus signing-link return.
Bundles three coherent changes:
1. Skatteverket extension (the main work)
- extensions.config.json: enable `skatteverket`, drop `invoice-inbox`
and `ai-agent` (those were enabled in config but lacked AWS env vars
in prod, so they loaded but failed at runtime)
- lib/reports/vat-declaration.ts: extend ACCOUNT_RUTA to populate
Ruta 06 (uttag 3401–3403), Ruta 20–24 (reverse-charge bases from
4xxx cost accounts), Ruta 50 (import 4545–4547), and Ruta 42
(3404/3994/3980); delete the supplier-type heuristic that made
Ruta 20 and Ruta 23 always 0
- extensions/general/skatteverket/lib/token-store.ts: work around
three real prod schema-drift issues — wrong column on read/delete
(was `company_id`, schema only has `user_id`), missing
UNIQUE(user_id) constraint that makes UPSERT fail (switched to
DELETE+INSERT), missing RLS policies (switched to service-role
client). Refresh path now reuses existing row's company_id when
none is passed.
- extensions/general/skatteverket/index.ts: 9 sites switched from
ctx.companyId to ctx.userId for the token-store key; pass
companyId from the OAuth callback
- extensions/general/skatteverket/types.ts + components/reports/
SkatteverketPanel.tsx: align field names with v1.0.24 RAML
(signeringsLank/kontrollResultat/resultat/kod/status/beskrivning).
Without this, the signing link never displayed.
- SkatteverketPanel: add Lås upp + Radera utkast + Hämta utkast +
Hämta beslut buttons so the full lifecycle is reachable from the UI
- lib/reports/__tests__/vat-declaration.test.ts: rewritten to match
the refactored calculator; new fixtures for cost-account-based
reverse charge (Ruta 20/21/22/23/24), Ruta 50 import, Ruta 06
uttag, Ruta 42 expansion; SKV §4.1.1.4 cross-field contract checks
- supabase/migrations/20260428120000_skatteverket_tokens_user_id_unique.sql:
idempotently adds the missing UNIQUE(user_id) constraint
- scripts/*: dev-only helpers used during the prod-of-test
verification (create test company, seed VAT data, inspect token
state, etc.)
2. Journal-entries cancelled-status filter
- app/api/bookkeeping/journal-entries/route.ts: when no status filter
is supplied, exclude `cancelled` entries by default
- supabase/migrations/20260428153500_journal_entries_with_related_exclude_statuses.sql
3. Swedish e-invoicing skill (reference docs only — no runtime code)
- .claude/skills/swedish-e-invoicing/
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(skatteverket): address PR review findings
- panel: handleFetchDraft read `result.data?.last` (typo) — switched to
`result.data?.locked` to match the field defined in
SkatteverketUtkastResponse and the v1.0.24 RAML. The "(låst)" suffix on
the success message would silently never appear before this fix.
- api-client: getValidToken had no concurrency guard, so two parallel
SKV requests from the same user could both call /token with the same
refresh_token. SKV rotates the refresh_token on first use, so the
second call would 401 with REFRESH_EXHAUSTED-adjacent failures. With
the new 6-button UI on SkatteverketPanel, rapid clicks made this a
realistic trigger. Added an in-process Promise map keyed on userId
that coalesces concurrent refresh attempts; cross-process races are
mitigated by re-reading tokens inside the critical section before
calling refreshAccessToken (if another process refreshed already, we
use the newer token instead of burning the old refresh_token).
- migration 20260428120000: dedup query used `created_at < max(...)`,
which failed to remove duplicates inserted in the same second. The
subsequent ALTER TABLE … ADD CONSTRAINT would then abort. Switched
to ctid (Postgres physical row identifier) to break timestamp ties.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(skatteverket): throw on token-store SELECT error before destructive DELETE
The company_id pre-read in storeTokens used destructuring that discarded
the error field. If the service-role SELECT failed for any reason (network
blip, overloaded DB, transient permissions issue), `existing` became null,
`resolvedCompanyId` stayed undefined, and execution fell through to the
DELETE. The old row got deleted successfully, then the INSERT omitted
company_id and failed with the NOT NULL constraint violation — leaving
the user with no token row at all and forcing a fresh BankID handshake.
Now we capture the SELECT error and throw before the DELETE runs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
103 lines
3.3 KiB
TypeScript
103 lines
3.3 KiB
TypeScript
/**
|
|
* Update company_settings.org_number for a given company.
|
|
*
|
|
* The Skatteverket validate handler reads org_number from company_settings
|
|
* (not companies), so this is what actually controls the redovisare sent
|
|
* to SKV. Mirrors set-arcim-org-number.ts but for the settings table.
|
|
*
|
|
* Usage: npx tsx scripts/set-company-settings-org.ts <COMPANY_ID> <ORG_NUMBER_10DIGIT> [--force]
|
|
*/
|
|
|
|
import { createClient } from '@supabase/supabase-js'
|
|
import { config } from 'dotenv'
|
|
import { resolve } from 'node:path'
|
|
|
|
config({ path: resolve(process.cwd(), '.env.local') })
|
|
|
|
const supabase = createClient(
|
|
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
process.env.SUPABASE_SERVICE_ROLE_KEY!,
|
|
{ auth: { persistSession: false } },
|
|
)
|
|
|
|
const [, , companyId, orgRaw, ...flags] = process.argv
|
|
const force = flags.includes('--force')
|
|
|
|
if (!companyId || !orgRaw) {
|
|
console.error('Usage: npx tsx scripts/set-company-settings-org.ts <COMPANY_ID> <ORG_NUMBER_10DIGIT> [--force]')
|
|
process.exit(1)
|
|
}
|
|
|
|
const org = orgRaw.replace(/[-\s]/g, '')
|
|
if (!/^\d{10}$/.test(org)) {
|
|
console.error(`Org number must be 10 digits (got ${org.length})`)
|
|
process.exit(1)
|
|
}
|
|
|
|
async function main() {
|
|
const { data: before, error: readErr } = await supabase
|
|
.from('company_settings')
|
|
.select('id, company_id, org_number, entity_type')
|
|
.eq('company_id', companyId)
|
|
.maybeSingle()
|
|
if (readErr) {
|
|
console.error(`read failed: ${readErr.message}`)
|
|
process.exit(1)
|
|
}
|
|
|
|
if (!before) {
|
|
// No settings row yet — need to insert. Pull entity_type from companies.
|
|
const { data: company } = await supabase
|
|
.from('companies')
|
|
.select('id, name, entity_type')
|
|
.eq('id', companyId)
|
|
.single()
|
|
if (!company) {
|
|
console.error(`Company ${companyId} not found`)
|
|
process.exit(1)
|
|
}
|
|
console.log(`No company_settings row for ${company.name} — inserting one.`)
|
|
const { error: insertErr } = await supabase
|
|
.from('company_settings')
|
|
.insert({ company_id: companyId, org_number: org, entity_type: company.entity_type })
|
|
if (insertErr) {
|
|
console.error(`insert failed: ${insertErr.message}`)
|
|
process.exit(1)
|
|
}
|
|
console.log(`✓ Inserted company_settings with org_number=${org}, entity_type=${company.entity_type}`)
|
|
return
|
|
}
|
|
|
|
console.log(`Before:`)
|
|
console.log(` company_id: ${before.company_id}`)
|
|
console.log(` org_number: ${before.org_number ?? '(null)'}`)
|
|
console.log(` entity_type: ${before.entity_type}`)
|
|
|
|
if (before.org_number === org) {
|
|
console.log(`\nNo change — already ${org}.`)
|
|
return
|
|
}
|
|
if (before.org_number && !force) {
|
|
console.error(`\nAlready set to ${before.org_number}. Pass --force to overwrite.`)
|
|
process.exit(1)
|
|
}
|
|
|
|
const { data: after, error: updateErr } = await supabase
|
|
.from('company_settings')
|
|
.update({ org_number: org })
|
|
.eq('company_id', companyId)
|
|
.select('org_number, entity_type')
|
|
.single()
|
|
if (updateErr || !after) {
|
|
console.error(`update failed: ${updateErr?.message}`)
|
|
process.exit(1)
|
|
}
|
|
const redovisare =
|
|
after.entity_type === 'aktiebolag' ? `16${after.org_number}` : `(EF prefix)`
|
|
console.log(`\nAfter:`)
|
|
console.log(` org_number: ${after.org_number}`)
|
|
console.log(` → redovisare for SKV API: ${redovisare}`)
|
|
}
|
|
|
|
main().catch(err => { console.error(err); process.exit(1) })
|