Fix/chrome pdf preview csp (#572)

* feat: add option to exclude year-end closing entries in SIE export and related reports

* delete docs

* fix: allow Chrome's PDF viewer in verifikat document preview

The /api/documents/:id/inline route shipped with
`object-src 'none'` in its CSP, which blocked Chrome's built-in PDF
viewer (it renders inline PDFs via an internal <embed>). Users on
Chrome saw "Det här innehållet har blockerats" when expanding a PDF
attachment in the bookkeeping view; Firefox (PDF.js) and Edge (own
viewer) were unaffected, and JPGs worked because <img> isn't subject
to object-src.

Drops the CSP for this route to the minimum needed for embeddability:
`frame-ancestors 'self'`. X-Content-Type-Options: nosniff plus the
fixed Content-Type from the handler already block MIME confusion;
X-Frame-Options: SAMEORIGIN + frame-ancestors still block clickjacking.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(auth): add webmail deep link to email confirmation screens

Mirrors Stripe's signup UX: after asking the user to verify their email,
detect their webmail provider from the domain and show a button that
opens the inbox in a new tab. Gmail gets a from:<sender> search
pre-populated; Outlook/Yahoo/iCloud/Proton open the inbox directly.
Unknown / custom domains fall back to the existing copy.

Sender address is configurable via NEXT_PUBLIC_BRANDING_AUTH_EMAIL_FROM
(default noreply@gnubok.se) so white-label installs can match their
Supabase Auth SMTP config.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(auth): unblock first-time password set for BankID users with MFA

Supabase rejects updateUser({password}) and mfa.unenroll with "AAL2 session
is required" whenever a TOTP factor is enrolled. BankID magic-link logins
produce AAL1, and middleware skips MFA enforcement for bankid_linked users,
so they had no path to AAL2 — leaving them unable to set a backup password
or disable MFA without going through the email-recovery escape hatch.

- /api/account/password: branch on app_metadata.has_password. First-time set
  writes via service.auth.admin.updateUserById (no existing credential to
  protect, AAL2 guard does not apply). Change-password keeps the user-session
  updateUser so AAL2 still fires for credential rotation.
- /mfa/verify: accept a safeReturnTo query param and route there after
  successful verify, so step-up flows can land back where they came from.
- SecuritySettings: detect the AAL2 error from both change-password and
  mfa.unenroll and redirect through /mfa/verify?returnTo=/settings/account
  instead of toasting a dead-end error.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add tests and rounding utility for öre precision in bokslut calculations

- Implemented `roundOre` function for rounding SEK amounts to two decimal places, ensuring consistent monetary calculations.
- Introduced `ORE_TOLERANCE` constant for comparing rounded amounts, facilitating invariant checks in financial entries.
- Created comprehensive tests for `roundOre`, covering typical cases, edge cases, and idempotency.
- Added year-end invariants tests to verify database-level guarantees for closing entries, ensuring they balance to the öre and reject discrepancies.
- Developed end-to-end tests for the dispositions chain, validating the correctness of calculations across various scenarios.

* fix: update PDF rendering to remove Swish QR code generation and set default to disable Swish visibility

* fix: enhance security by rejecting data URIs in safeReturnTo function tests

* fix: improve rounding logic in roundOre function and add customer_type migration

* fix: add customer_type column to customers and enforce CHECK constraint

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mattsson
2026-05-26 22:29:41 +02:00
committed by GitHub
co-authored by Claude Opus 4.7
parent 0087b7be3f
commit 32d9978f1b
207 changed files with 24187 additions and 1052 deletions
+170 -7
View File
@@ -1,6 +1,12 @@
// Entity types
export type EntityType = 'enskild_firma' | 'aktiebolag'
// Swedish accounting framework. K2 (BFNAR 2016:10) is the default simplified
// ruleset for smaller AB; K3 (BFNAR 2012:1) is the principles-based ruleset
// required for medium-to-large AB and permitted voluntarily for smaller ones.
// Only meaningful for entity_type='aktiebolag'.
export type AccountingFramework = 'k2' | 'k3'
// Company role for multi-tenant access
export type CompanyRole = 'owner' | 'admin' | 'member' | 'viewer'
@@ -23,6 +29,7 @@ export interface Company {
name: string
org_number: string | null
entity_type: EntityType
accounting_framework: AccountingFramework
created_by: string
team_id: string | null
archived_at: string | null
@@ -240,6 +247,14 @@ export interface CompanySettings {
// Voucher series
default_voucher_series: string
/**
* Per-source-type default voucher series map. Keys are
* JournalEntrySourceType values; values are single uppercase letters A–Z.
* Resolved by `lib/bookkeeping/voucher-series-resolver.ts`. Defaults to
* all "A" entries; users can override per source via the bookkeeping
* settings UI.
*/
default_voucher_series_per_source_type: Partial<Record<JournalEntrySourceType, string>>
// Invoice PDF settings
ore_rounding: boolean
@@ -253,9 +268,23 @@ export interface CompanySettings {
invoice_late_fee_text: string | null
invoice_credit_terms_text: string | null
// Invoice branding (per-company colors, font, optional header/footer text).
// Defaults preserve the legacy hardcoded palette so unbranded companies
// render identically to the pre-branding template.
invoice_primary_color: string // hex #RRGGBB, default '#1a1a1a'
invoice_accent_color: string // hex #RRGGBB, default '#666666'
invoice_font_family: 'Helvetica' | 'Times-Roman' | 'Courier'
invoice_header_text: string | null
invoice_footer_text: string | null
// Automation
send_invoice_reminders: boolean
// Reminder surcharges (dröjsmålsränta + lagstadgad påminnelseavgift)
reminder_fee_enabled: boolean
reminder_fee_amount: number
reminder_interest_rate_override: number | null
// Logo
logo_url: string | null
@@ -701,7 +730,7 @@ export interface Invoice {
// Credit note reference
credited_invoice_id: string | null
// Document type (invoice, proforma, delivery_note)
// Document type (invoice, proforma, delivery_note, quote)
document_type: InvoiceDocumentType
// Conversion tracking (proforma -> invoice)
@@ -712,6 +741,16 @@ export interface Invoice {
paid_amount: number | null
remaining_amount: number
// ROT/RUT-avdrag claim info. `deduction_total` is the sum of the per-item
// deduction_amount and equals the 1513 debit on the verifikation. The
// personnummer is stored only as AES-256-GCM ciphertext + the last four
// digits (PII isolation). All three fields are null/0 on invoices with
// no ROT/RUT lines. Optional in TypeScript to keep legacy fixtures
// (pre-migration) valid — treat undefined the same as 0/null.
deduction_total?: number
deduction_personnummer_encrypted?: string | null
deduction_personnummer_last4?: string | null
created_at: string
updated_at: string
@@ -746,6 +785,27 @@ export interface InvoiceItem {
vat_rate: number
vat_amount: number
// ROT/RUT-avdrag (Sweden's tax deduction for household services / home
// renovation). When `deduction_type` is set, the system computes
// `deduction_amount` from the rules in lib/invoices/rot-rut-rules.ts
// and posts the receivable to BAS 1513 (Skatteverket). v1 deducts on
// the full line total; future work can use `labor_hours` to honour the
// labor-only restriction.
//
// All fields are optional in TypeScript even though Postgres has
// defaults — legacy rows pulled before the schema change carry
// `undefined` in JS land, and many existing test fixtures predate the
// ROT/RUT migration. Treat undefined the same as null/0 throughout.
deduction_type?: 'rot' | 'rut' | null
deduction_amount?: number
labor_hours?: number | null
/** Skatteverket arbetstypskod (e.g. 'BYGG', 'STAD'). See ROT_WORK_TYPES / RUT_WORK_TYPES. */
work_type?: string | null
/** Fastighetsbeteckning. Required for ROT, optional for RUT. */
housing_designation?: string | null
/** Lägenhetsnummer. Optional, used for ROT in flerbostadshus. */
apartment_number?: string | null
created_at: string
}
@@ -897,6 +957,10 @@ export interface CreateInvoiceInput {
your_reference?: string
our_reference?: string
notes?: string
/** Plaintext personnummer — encrypted server-side before storage. */
deduction_personnummer?: string
/** Fastighetsbeteckning. Required when any item carries deduction_type === 'rot'. */
deduction_housing_designation?: string
items: CreateInvoiceItemInput[]
}
@@ -906,6 +970,12 @@ export interface CreateInvoiceItemInput {
unit: string
unit_price: number
vat_rate?: number
/** ROT/RUT toggle. null/undefined = no deduction. */
deduction_type?: 'rot' | 'rut' | null
labor_hours?: number | null
work_type?: string | null
housing_designation?: string | null
apartment_number?: string | null
}
export interface CreateTransactionInput {
@@ -1031,6 +1101,7 @@ export type JournalEntrySourceType =
| 'supplier_invoice_privately_paid'
| 'supplier_credit_note'
| 'currency_revaluation'
| 'reminder_fee'
// Journal entry status
export type JournalEntryStatus = 'draft' | 'posted' | 'reversed' | 'cancelled'
@@ -2310,7 +2381,6 @@ export interface AuditLogEntry {
export interface CostCenter {
id: string
user_id: string
company_id: string
code: string
name: string
@@ -2321,7 +2391,6 @@ export interface CostCenter {
export interface Project {
id: string
user_id: string
company_id: string
code: string
name: string
@@ -2390,6 +2459,13 @@ export interface YearEndResult {
nextPeriod: FiscalPeriod
openingBalanceEntry: JournalEntry
revaluationEntry: JournalEntry | null
/**
* IB/UB reconciliation per balance sheet account, computed after the
* opening balances are posted. Surfaced to the UI's ResultStep so the
* user can verify continuity before navigating away. Always within
* ORE_TOLERANCE — otherwise executeYearEndClosing would have thrown.
*/
continuity?: ContinuityCheckResult
}
// ============================================================
@@ -2410,6 +2486,32 @@ export type DepreciationMethod =
| 'linear'
| 'declining_balance_30'
| 'declining_balance_20'
| 'restvardesavskrivning_25'
/**
* K3 component (BFNAR 2012:1 ch 17.4 — komponentavskrivning). When a
* substantial asset (typically real estate) has significant components with
* materially different useful lives, K3 reporting requires each component to
* be depreciated on its own life rather than treating the asset as a single
* unit. Components are stored as an array on `Asset.k3_components`; when
* non-null, the depreciation engine routes through `computeComponentDepreciation`
* and sums per-component linear depreciation (with the same pro-ration logic
* as the asset-level linear method).
*
* Validation (enforced in `lib/bokslut/assets/k3-components.ts`):
* - sum(components.cost) === asset.acquisition_cost (±1 kr tolerance)
* - every component: cost > 0, useful_life_months > 0
* - salvage_value (if present) ≤ component cost
* - non-empty array when set to non-null
*
* Salvage_value defaults to 0 when omitted.
*/
export interface K3Component {
name: string
cost: number
useful_life_months: number
salvage_value?: number
}
export interface Asset {
id: string
@@ -2425,11 +2527,37 @@ export interface Asset {
bas_asset_account: string
bas_accumulated_account: string
bas_expense_account: string
/** Book-value floor for restvärdeavskrivning (IL 18 kap 13§ st.3). Required
* iff depreciation_method = 'restvardesavskrivning_25'; null otherwise. */
restvarde_target: number | null
disposed_at: string | null
disposed_proceeds: number | null
/** Reserved for K3 component depreciation (BFNAR 2012:1 ch.17.4). Empty
* / null for K2 — Phase 5 will fill this when K3 ships. */
k3_components: unknown | null
/** Output VAT on disposal proceeds (ML 3 kap 3 § / 7 kap 3 §). Defaults to
* 0 — only nonzero when the sale was momspliktig. The VAT account
* (2611/2621/2631) is derived from disposed_vat_treatment. */
disposed_proceeds_vat: number
/** VAT treatment applied to disposal proceeds. Null for legacy disposals
* without VAT data. Constrained by DB CHECK to the same enum as
* VatTreatment. */
disposed_vat_treatment: VatTreatment | null
/** Jämkning amount per ML 8a kap 7 § — input VAT paid back on disposal
* inside the correction period. Defaults to 0; positive number = debt
* to the state booked on 2641 credit. */
jamkning_amount: number
/** Remaining months in the korrigeringstid at disposal date. Audit
* metadata only — the booking sits on the journal entry. */
jamkning_remaining_months: number | null
/** Total korrigeringstid in months: 60 (lös egendom) or 120 (fastighet /
* markanläggning). Audit metadata. */
jamkning_total_months: number | null
/** Original input VAT that was deducted at acquisition. Audit metadata
* the user supplies (or the system derives from the supplier invoice). */
jamkning_original_input_vat: number | null
/** K3 component depreciation (BFNAR 2012:1 ch.17.4). When non-null, the
* depreciation engine sums per-component linear depreciation instead of
* applying `depreciation_method` to the asset as a whole. Null for K2
* companies (the API rejects writes for accounting_framework='k2'). */
k3_components: K3Component[] | null
notes: string | null
created_at: string
updated_at: string
@@ -2529,6 +2657,13 @@ export interface InvoiceReminder {
action_token: string
action_token_used: boolean
created_at: string
// Dröjsmålsränta + lagstadgad påminnelseavgift (Räntelagen §6, Lag 1981:739)
interest_amount: number
interest_rate: number | null
interest_from_date: string | null
interest_days: number | null
reminder_fee: number
fee_journal_entry_id: string | null
}
// Swedish labels for reminder levels
@@ -2696,7 +2831,10 @@ export type AGIStatus =
| 'rejected' // reserved (kontrollresultat DONE_REJECTED could land here)
export type SalaryLineItemType =
| 'monthly_salary' | 'hourly_salary' | 'overtime' | 'bonus' | 'commission'
| 'monthly_salary' | 'hourly_salary'
| 'overtime' | 'overtime_50' | 'overtime_100'
| 'ob_weekday_evening' | 'ob_weekend' | 'ob_night' | 'ob_holiday'
| 'bonus' | 'commission'
| 'gross_deduction_pension' | 'gross_deduction_other'
| 'benefit_car' | 'benefit_housing' | 'benefit_meals' | 'benefit_wellness' | 'benefit_bike' | 'benefit_other'
| 'sick_karens' | 'sick_day2_14' | 'sick_day15_plus'
@@ -2707,6 +2845,31 @@ export type SalaryLineItemType =
| 'net_deduction_other'
| 'correction' | 'other'
export type ShiftPremiumItemType =
| 'overtime_50' | 'overtime_100'
| 'ob_weekday_evening' | 'ob_weekend' | 'ob_night' | 'ob_holiday'
export interface ShiftPremiumRule {
id: string
company_id: string
name: string
applies_to_all_employees: boolean
applies_to_employee_ids: string[]
/** ISO weekday array: 1 = Monday … 7 = Sunday. */
day_of_week: number[]
/** 'HH:MM' or 'HH:MM:SS' (PostgreSQL TIME). */
start_time: string
/** 'HH:MM' or 'HH:MM:SS'. End values <= start mean the window wraps midnight. */
end_time: string
premium_percent: number
item_type: ShiftPremiumItemType
priority: number
is_active: boolean
created_at: string
updated_at: string
created_by: string | null
}
export interface Employee {
id: string
company_id: string