fea5dfd1f912ec1111cec662cd8362f9aa55dfd9
162 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f0bedc14af |
feat(payments): betalfil UI (betalfil 3/3) (#1505)
* feat(payments): betalfil UI (betalfil 3/3) Bulk-select + Skapa betalfil bulkbar on the supplier-invoices list, preview dialog with per-line editable amount/date and exclusion reasons, payment-files history page with re-download, cancel and a sequential bulk mark-paid (duplicate guard respected, never forced), I betalfil chip on rows in active batches, clearing/kontonummer fields on the supplier form, and the supplier_payment_files namespace in sv+en. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(sandbox): make the demo AP data betalfil-ready Demo supplier bankgiro numbers were not Luhn-valid, the unpaid demo invoice had remaining_amount 0 (no trigger derives it, so the list said 0 kr kvar att betala), and the company had no IBAN/BIC, all of which excluded the seeded data from the betalfil flow. Numbers swapped for Luhn-valid ones (991-2346 is Bankgirot's test number), a valid OCR added, and both bulk-insert rows set remaining_amount explicitly per the PostgREST normalization rule already documented inline. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
576a34750a |
feat(payments): payment-batch API surface (betalfil 2/3) (#1504)
Preview/create/list/get/file/cancel routes under /api/supplier-invoices/payment-batches, SI_BATCH_* structured errors, and a fail-closed batch-membership pre-check in the supplier invoice DELETE route (the FK RESTRICT is the backstop). File downloads stamp file_generated_at + download_count but regenerate byte-identically. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c77367ac1a |
feat(audit): log trusted-role committed_at overrides to behandlingshistorik (#1490)
* feat(audit): log trusted-role committed_at overrides to behandlingshistorik (#1444) Migrations 20260806150000/160000 let backend writers (service_role, no-claims direct SQL) preserve a preset committed_at on the draft-to-posted transition, but nothing recorded that the transition timestamp was overridden: BFNAR 2013:2 kap 8 wants a log of who did what, when. If a trusted-role connection is ever used against a live company, there is now an audit row separating real from preset commit time. - audit_log accepts action COMMITTED_AT_OVERRIDE (types + Zod filter too) - log_committed_at_override() SECURITY DEFINER writer: audit_log has RLS with no INSERT policy and service_role is not guaranteed BYPASSRLS in every harness; EXECUTE revoked from anon/authenticated so PostgREST cannot expose it as an RPC - set_committed_at() calls the writer in the preserve branch; the stamping branch is unchanged from 20260806160000 - pg tests: override row content (preset value, wall clock, jwt role) for postgres and service_role writers, absence on the stamp paths, and the writer's privilege lockdown Closes #1444 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * review(audit): stamp wall_clock with clock_timestamp(), not now() Seeding flows post many entries inside one transaction; now() would pin every override row's wall_clock to the BEGIN instead of the actual transition moment. Captured once so new_state and description agree. Test posts inside an explicit transaction after pg_sleep and asserts wall_clock moved past the transaction start. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
7411a0171b |
feat(mileage): körjournal with milersättning booking, MCP tools and CSV export (#1448)
* feat(mileage): körjournal with milersättning booking, MCP tools and CSV export New mileage_trips table (RLS, booked-delete trigger per BFL retention), lib/mileage service reusing the payroll schablon rates, /api/mileage routes (trips CRUD, period booking to 7331, salary-run push, körjournal CSV), Körjournal dashboard page + nav, and three staged MCP tools (search-only catalog). Trips book as one verifikat per period via the engine; salary path inserts mileage_taxfree line items. mileage_trips classified in the full-archive export. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(mileage): use shared roundOre helper per tightened ratchet baseline Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(mileage): pending_operations op-type migration + Swedish review findings - New migration pair adds log_mileage_trip/book_mileage_period to the pending_operations operation_type CHECK (pg-real audit). - bookMileagePeriod refuses a period spanning several employees and names the employee in the verifikationstext when scoped (BFL motpart). - vehicle_registration required for förmånsbil trips (schema, service, MCP staging, UI surfaces the field). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(mileage): claim-first booking, CSV injection guard and driver column - bookMileagePeriod claims trips (draft to booked CAS) before creating the verifikat, so a concurrent second booking loses the race instead of double-booking; claim reverts if verifikat creation fails. - Körjournal CSV neutralizes formula-injection triggers (OWASP) and adds a Förare column naming the employee per trip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(mileage): resolve CodeRabbit + Swedish review round: race, drift and hardening - Copying a round trip no longer re-doubles the stored distance. - pushMileageToSalaryRun claims trips before inserting line items (retry can no longer double-pay); CLAIM_LOST replaces misleading NO_TRIPS on lost races. - Booked trips are DB-immutable via a BEFORE UPDATE trigger (new migration 20260807113215): only claim/link/revert transitions and notes edits pass. - Cross-year periods rejected (schablon rates are per calendar year); payroll config year read from the date string, not TZ-dependent getFullYear(). - MCP staged bookings freeze the previewed trip set (trip_ids in params) and the commit fails on drift; validation errors return 400, not 500. - PATCH enforces the förmånsbil regnr rule on the effective row; export validates dates before they reach the Content-Disposition header; employee_id is verified company-scoped on trip creation; stale orphaned claims released. - UI: fetch flags reset in finally; ICU plural for draft summary; distance stored at the column's 1-decimal precision. - Tests: [id] route suite, pushMileageToSalaryRun suite, claim-race, drift, cross-year and update-trigger pg cases. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(mileage): revert-to-draft must clear salary_run_id at the trigger level New migration 20260807114924 replaces the booked-immutability function: a booked -> draft revert now rejects rows keeping salary_run_id, closing the DB-level double-pay path CodeRabbit flagged. pg test pins both directions; the CLAIM_LOST unit test now asserts the revert. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(mileage): company-scope employee_id on PATCH (Superagent P2) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(mileage): valid v4 uuid in cross-company employee PATCH test Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a0ca692fed |
feat(invoices): quarterly, half-yearly and yearly recurring invoice schedules (#1438)
* fix(mcp): offer the link tool in the uncategorized-transactions VAT blocker The gnubok_vat_close_check blocker hint only named categorize/auto-match, both of which create new bookkeeping. For a transaction whose affarshandelse is already booked on an existing verifikat, following the hint would double-book, so agents dead-ended the case into "contact support" (2026-08-06 support mail from Orto Engineering). The hint now also names gnubok_link_transaction_to_journal_entry, is extracted as an exported constant pinned by a test, and the tool joins the categorize_month recommended loadout. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(invoices): quarterly, half-yearly and yearly recurring schedules User request: recurring invoice schedules only supported monthly cadence. Adds interval_months (SMALLINT 1-12, default 1) to recurring_invoice_schedules; the UI offers manadsvis/kvartalsvis/ halvarsvis/arsvis presets while API and MCP accept any 1-12. The cron advances next_run_date by whole intervals from the due date, and the new rollNextRunDateForward() helper rolls missed or edited interval schedules on their own month grid so a quarterly Jan/Apr/Jul/Oct schedule missed in an outage rolls Jan 15 to Apr 15, never Feb 15. Monthly (interval 1) keeps its existing today-anchored recompute semantics unchanged. Changing the interval alone never touches next_run_date: the new cadence applies from the next run, so an edit can never pull a send earlier. Existing rows default to 1 and behave byte-identically. The MCP slice of this feature (interval_months on the three recurring-schedule tools in server.ts) was committed in d2600907f alongside the VAT-blocker hint fix by a parallel session sharing this worktree. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(invoices): address PR #1438 review findings CodeRabbit round 1, all three findings: - MCP descriptions now state the full accepted interval range (any integer 1-12) instead of enumerating only the 1/3/6/12 presets, and qualify that changing ONLY interval_months leaves next_run_date untouched. - assertValidCadence rejects fractional day_of_month. - rollNextRunDateForward rejects calendar-invalid anchors that pass the shape regex (2026-13-05, 2026-02-31), with regression tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d1c411ad6f |
feat(invoice-inbox): surface WhatsApp chat context in booking flows (#1339)
* feat(invoice-inbox): surface WhatsApp chat context in booking flows
The WhatsApp intake bot writes verified human answers (photo caption,
representation deltagare + syfte, sender note, open-question state) to
invoice_inbox_items.channel_context. This makes the in-app booking flows
READ it:
- New core renderer lib/documents/channel-context-notes.ts: deterministic
compact Swedish line ("Representation: Anna Berg (Volvo), Jakob W ·
Syfte: uppföljning av avtal"), capped at 220 chars by dropping whole
participant names ("… och N till"), never mid-name. Representation
first, then user_note; caption only when nothing else exists.
- FieldsRail "Från WhatsApp" block in InvoiceInboxWorkspace: caption,
deltagare, syfte, anteckning rows plus an ochre AttnLine when a chat
question expired unanswered (pending_question.status = moved_to_app).
- Notes threading: book-direct and convert default their notes to the
rendered string server-side when the request carries none (a supplied
value always wins); BookDirectlyDialog prefills its notes input with
the same string so the user can edit it before it lands. Bulk-book
(categorize-core) joins the shared batch note with the per-item
rendered context so the representation trail survives batch booking.
- Inbox list: whatsapp rows get a chat icon and a quiet "Fråga obesvarad"
badge for moved_to_app items. No worklist count change: unresolved
whatsapp items are already counted by countInboxDocuments.
- sv/en strings for every new key; renderer + route + bulk tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(invoice-inbox): honor cleared notes and keep unreviewed captions out of verifikat
Two adversarial-review findings on the WhatsApp surfacing flows, both about
text that lands on an immutable verifikat.
1. A cleared note was silently re-applied. BookDirectlyDialog prefills the
rendered chat context, and the dialog sent `notes.trim() || undefined`
while book-direct and convert defaulted from channel_context on any falsy
value. A user who read the prefill, disagreed and deleted it therefore got
it written back onto a posted entry, removable only through a formal
rättelse. Both code comments claimed "an edited value always wins", which
was false for exactly that edit. Now PRESENCE of the field decides: the
dialog always submits `notes` (empty string included) and the routes only
default when the field is absent from the request (MCP, older clients).
The Zod `.optional()` carrying that distinction is documented at the
schema so it is not "tidied" into a `.default('')` later.
2. The photo caption was auto-burned into verifikat text with no review.
renderChannelContextNotes fell back to the raw caption, and bulk-book
appended the result per item with no per-item notes field at all (the MCP
approval preview deliberately shows no per-item PII either), so unreviewed
chat text reached a WORM record nobody had seen. The renderer now takes
{ includeCaption } and leaves the caption out by DEFAULT: representation
answers and user_note are replies to a question the bot asked, the caption
is not. Only the Bokför direkt prefill opts in, where the user reads the
string in an editable field before booking.
Tests: cleared-notes and whitespace-cleared on book-direct, cleared-notes on
convert, caption-never-defaulted on both routes, caption-not-threaded in
bulk-book, and the renderer's opt-in. The book-direct cleared-notes tests
were mutation-checked (restoring the truthiness fallback fails them).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(reports): export the chat answers behind a verifikat in the full archive
The verifikat line caps the representation trail at 220 chars and drops whole
participant names ("… och N till"); the complete list (deltagare, syfte,
raw_answer) exists only in invoice_inbox_items.channel_context. That table was
in ARCHIVE_EXCLUDED_TABLES with a rationale predating channel_context ("inbox
workflow state"), so a company leaving Accounted and keeping the full-archive
export as its BFL 7-year record kept an incomplete deltagare documentation for
its representation deductions.
Dumped as a column PROJECTION, not the whole row: the new
MasterDataTableSpec.columns narrows the select to the underlag provenance
(document, matched transaction, created verifikat / leverantörsfaktura) plus
channel_context, so the answers are tied to what was booked from them while
the inbox workflow state (email bodies, OCR output, error messages) stays out
of the archive. The documents themselves remain in dokument/.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Jakob Wennberg <jakob.wennberg@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
86c6af6976 |
feat(api): v1 REST company-settings write endpoint (PATCH) (#1405)
* feat(api): v1 REST company-settings write endpoint (PATCH)
Adds PATCH /api/v1/companies/{companyId}/settings, closing the gap where
the v1 REST surface had no company-settings write (only the staged MCP
tool gnubok_update_company_settings could change them).
- Field set is identical to the MCP tool: payment details (bank account,
bankgiro, plusgiro, swish, iban, bic), invoice contact details (email,
phone, website), contact_person (aliased onto default_our_reference,
exactly as the MCP tool maps it), and invoice_email_texts.
- Validation reuses the shared UpdateCompanySettingsParamsSchema (Luhn
bankgiro/plusgiro, invoice email placeholder whitelist), so REST and
MCP can never drift apart on the Swedish-domain rules.
- Writes directly with an explicit .eq('company_id', ...) filter,
following the v1 customers PATCH precedent: no staged operation, since
REST callers are already gated by the companies:write scope.
- Dry-runnable, mandatory Idempotency-Key, registered in the endpoint
catalogue, scope map, and load-routes; spec snapshot updated.
- The companies:write scope description now mentions the REST endpoint.
No GET endpoint yet (possible follow-up); reads stay on the MCP tool.
Fixes #1348
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(v1): harden company-settings PATCH contract, align risk tier
Adversarial-review follow-up for the settings PATCH endpoint (#1348):
- Declare risk: 'medium' in registerEndpoint, matching the
update_company_settings tier in lib/pending-operations/risk-tiers.ts
(payment settings control where customers send money on future
invoices). The spec snapshot does not pin the risk field, so no
snapshot regeneration is needed.
- Pin the partial-PATCH contract: every column the caller did not
supply must arrive as undefined in the update payload, never null.
A future ?? null on the literal 13-column payload would silently
clear every unsupplied column; the new test fails on exactly that
regression (verified by mutation).
- Cover the body-parsing branches: invalid JSON and non-object JSON
bodies (bare array, string, number, null) each return 400 with the
handler's respective message and never reach the update call.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
9f5a43310b |
fix(salary): recompute entitled_days on existing ledger rows and record pre-cutover taken days (#1403)
The vacation ledger sync carried entitled_days verbatim on existing open rows while re-deriving accrued and taken, so a stale entitled value (for example the flat 25 stored before Semesterlagen 7 § pro-rating existed) survived every sync. The recompute loop now re-derives entitled the same way the lazy-seed path does, with the opening-balance cutover still outranking recomputation for the year containing cutover_date. Opening balances could also not record paid vacation days already taken in the cutover year under the previous payroll system. New additive column employee_opening_balances.vacation_days_taken_this_year (NUMERIC NOT NULL DEFAULT 0, CHECK 0..40) threaded through the shared service, the Zod schema, the MCP staging tool (schema + mergeable fields), the staged-operation executor, the v1 REST routes, and the employee editor form. Ledger semantics for the cutover year, on both seed and recompute paths: entitled = remaining + taken_this_year, taken = booked-run taken + taken_this_year, so remaining keeps meaning remaining and the seeded value survives every subsequent sync. Fixes #1347 Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
00ae3540db |
feat(customers): carry contact person and invoice copy recipients through migration (#1392)
* feat(customers): carry contact person and invoice copy recipients through migration Extends the arcim-migration entity mapper, Fortnox provider mapper, canonical DTOs, customer APIs (web + v1) and invoice send flows so contact person and customer-level invoice CC/BCC addresses survive provider migrations. NULL means unconfigured and empty means an explicit clear, so re-syncs enrich legacy gaps without resurrecting deliberately removed values. Fortnox fixed assets are split into a dedicated follow-up issue. Fixes #1345 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(db): bump customer metadata migration past pack-slug version Main already contains 20260803230000; keep new versions strictly newest so Supabase branching applies them in order. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(customers): complete Customer type consumers and make enrichment payload resolvable The preview-pdf mock customer and the makeCustomer fixture now carry the three new metadata fields, fixing the type-check failure in Build (zero extensions) and Vercel. The enrichment update in the migration orchestrator now spells its payload as an object literal typed CustomerMetadataEnrichment (absent keys drop at serialization), so the phantom-column guard resolves the columns instead of counting another unresolvable dynamic payload past its ceiling. The cc/bcc guards also verify element types instead of casting. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
16fbcefbbc |
feat(invariants): shared format contracts + upgrade-path CI (#1364)
* feat(invariants): centralise shared format contracts, reconcile the org-number paths
The same format rules were written out independently across the codebase, and
where they disagreed the disagreement was invisible until a filing failed.
Worst case, now fixed: four Skatteverket- and Bolagsverket-bound export paths
each had their own idea of a valid organisationsnummer.
lib/skatteverket/format.ts strip '-' only threw on any input with a space
lib/salary/ku/ku10-generator.ts replace('-', '') first hyphen only, spaces survived
lib/salary/agi/xml-generator.ts strip non-digits stray letters passed the length check
lib/bokslut/ixbrl/validate /^\d{6}-?\d{4}$/ rejected the 12-digit form, no Luhn
A company stored with a space or in 12-digit form could file AGI all year and
then fail at the arsredovisning deadline with a message that did not say why.
lib/invariants/ now owns account number, ISO date, four-digit fiscal year and
org number, each with the rationale recorded next to the rule. normalizeOrgNumber
moves here from lib/company-lookup/ and isSaneDateString from lib/utils.ts; both
old paths re-export, so no caller changes. lib/api/schemas.ts builds its
primitives on the module, so ~100 schemas inherit any correction.
The arsredovisning check-digit verdict is a warn, not an error: a wrong Luhn
digit is almost certainly a typo worth surfacing, but whether every org number
Bolagsverket accepts satisfies Luhn is a Swedish domain question we have not
verified against a primary source, and an error there blocks Skicka in. We do
not block a statutory filing on an unverified assumption.
KU10 still passes a 12-digit stored org number through unfolded. That is
pre-existing, and whether the KU10 schema wants 10 or 12 digits is not covered
by the swedish-payroll skill, so it is pinned by a test rather than changed
silently.
Guard 8 (hand-rolled-invariant) tracks the remaining 114 inline copies as a
ratchet that may only go down, same mechanism as the roundOre guard.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(ci): add an upgrade-path job that applies new migrations against real data
The pg-real job applies all 548 migrations to an EMPTY database. Empty means
zero rows, so a migration that adds a NOT NULL, adds a CHECK, creates a unique
index or backfills passes trivially in CI and can still fail on production,
where the rows exist. CI proved that a fresh install works; nothing proved that
an existing install upgrades.
The new pg-upgrade job: apply the schema as it stands at the merge base, seed a
small real company (three posted verifikat, balanced lines, one ore-level
amount), then apply ONLY the migrations this PR adds, then assert the data
survived (entries still posted, lines intact, ledger still balances, ore
unchanged, voucher numbers sequential). A PR with no migration no-ops.
Verified locally against supabase/postgres:15.8.1.060 rather than assumed, with
three deliberately bad migrations:
rescale money on posted lines empty: would pass seeded: ERROR (immutability trigger)
CHECK violating the ore row empty: exit 0 seeded: exit 3
NOT NULL on a populated column empty: exit 0 seeded: exit 3
Base migrations are read out of the merge-base git tree, not the working tree,
so a PR that edits an already-shipped migration still gets the original applied
and the edit surfaces as a failure here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: record the invariants and upgrade-CI decisions
Two entries covering what this PR changes and, more importantly, the calls that
are not obvious from the diff: why the arsredovisning check-digit verdict is a
warning rather than an error, why KU10's 12-digit passthrough is pinned instead
of fixed, and why the ROT/RUT brf org-number schemas stay on their own rule.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs(test): mark the upgrade fixture as CI-only, never a production template
The fixture writes posted journal_entries and their lines directly, bypassing
the engine and the atomic commit RPC. That is the only way to hand a migration
pre-existing posted rows to break, and it is safe against a throwaway CI
database, but it reads like a sanctioned pattern to anyone who finds it later.
Says so explicitly, with the reason it is confined here (no voucher sequence to
keep gapless, no retention obligation on a database destroyed with the job) and
a pointer back to Hard Rule 2 for anything touching a real database.
Raised by the Swedish compliance review bot on #1364.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
80a14ddfd2 |
feat(mcp): dimension parity for the write/read tool edges (#1274)
Closes the MCP dimension gaps found in the 2026-07-28 audit:
- gnubok_bulk_book_inbox_items accepts a shared dimensions bag through
all three layers (tool schema + BulkBookInboxSchema + categorize-core
BulkBookInboxInput), resolve-don't-select with echoed resolutions; the
web inbox bulk-book route and the pending-op executor inherit it via
the shared schema.
- gnubok_create_employee / gnubok_update_employee accept
default_dimensions (names resolve to codes; {} clears on update).
The command layer already persisted the field: only the MCP boundary
blocked it, leaving payroll tagging dashboard-only.
- gnubok_query_journal: dimensions bag filter (jsonb containment via
the GIN index, covers custom dims the legacy project/cost_center
filters cannot) + include_dimensions to return each line's bag.
The wide full-match fetch stays dims-free unless something needs it.
- gnubok_list_invoices / gnubok_list_supplier_invoices return
default_dimensions (agents could set invoice bags but never read
them back).
- Discoverability: create_voucher, categorize_transaction,
correct_entry, update_invoice descriptions now name dimensions;
categorize_month and invoice_run loadouts include
gnubok_list_dimensions. Trimmed new schema prose to stay under the
tools/list payload budget.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
222e581476 |
feat(categorize): dimension bags end-to-end + runtime template learning (#1273)
* feat(categorize): dimension bags end-to-end + runtime template learning The categorize path could not tag: categorize-core accepted a dimensions bag but no route or UI ever passed one, and runtime template learning dropped the bag entirely (only SIE import produced dimension-carrying patterns). - CategorizeTransactionSchema gains dimensions; the dashboard route, v1 single and v1 batch-categorize apply it to the mapping result's business lines (explicit bag wins over a learned counterparty bag). - categorization_templates.default_dimensions (migration 20260728091000) records the bag of the latest tagged booking; latest-explicit-wins, an untagged booking never erases it. Applied on the legacy single-line template path and the mirrored-refund path; multi-line SIE patterns keep their authoritative per-entry bags. - QuickReviewDialog gets a LineDimensionFields picker (dimensions_enabled gate, same as BulkBookDialog), prefilled from the counterparty suggestion's learned bag; hidden for multi-line patterns whose per-line bags would ignore an edit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Renumber migration above 20260728120000 (out-of-order vs prod after #1271) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
951b33363b |
feat(recurring): carry dimension bags on recurring invoice schedules (#1272)
Schedules and their template items now store {sie_dim_no: code} bags
(default_dimensions / dimensions), and the cron generator copies them
onto every spawned invoice + item, so recurring invoices book with the
same projekt/kostnadsstalle tags a manual invoice would. Wired through
the web CRUD routes, the staged-operation executors, and the MCP
create/update/list schedule tools (resolve-don't-select, resolutions
echoed in the preview).
Migration 20260728090000 adds the two jsonb columns (same shape+CHECK
as invoices/invoice_items, PR7 producer parity).
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
65c6d4c178 |
Fix/07 27 (#1271)
* fix(enable-banking): keep bank account mappings across reconnects and surface dead sessions A PSD2 reconnect silently moved the user's ledger mapping. Account identity came from the provider's account uid, which does not survive a re-authorization at every ASPSP, and a fresh connect to an already-connected bank mints a new bank_connections row regardless. Both paths looked like "an account we have never seen", so the allocator handed out the next free 19xx slot and a 1930/1940/1941 mapping came back as 1942-1946 on every consent renewal, roughly quarterly per connection. Match on the IBAN instead. resolvePsd2LedgerAccount() finds the existing cash_accounts row by normalized IBAN before allocating, and upsertFromPsd2 promotes that row in place rather than inserting a second one, so it keeps its id and its linked transactions and is re-pointed at the connection that just authorized. The previous holder's connection status is deliberately ignored: one IBAN is one physical account, and the old row often still reads 'active' because the bank killed the session without telling us. The allocator also stopped treating a 19xx number as free just because no cash_accounts row holds it. A chart imported from SIE carries the company's real bank accounts by name with no PSD2 row behind them, which is how a SEK company account got proposed as an unrelated brokerage account. Overflow now skips chart-occupied numbers, falling back only when nothing unnamed is left. Dead connections kept rendering as "Aktiv": status only ever changed when a transaction fetch failed, so a session killed bank-side stayed healthy-looking with a stale last_synced_at while the user read old balances as current. Add probeSessionHealth() and run it in the daily cron over every connection that run did not prove alive, including the ones the loop skips silently (capability gate, all accounts deselected) and the ones parked in pending_selection that the cron never looked at. It acts only on a definite dead answer; anything ambiguous leaves the row alone, since a wrong flip costs a full BankID re-authorization. The all-accounts-deselected branch is reclassified 'synced' to 'skipped' for the same reason: it never contacts the bank, so it must not count as proof of life. The settings row warns when an active connection has not synced in three days or has never synced. Which company a connection belongs to was invisible. Everything was already scoped to ctx.companyId, so there was no cross-tenant leak, but a bank authorized while the wrong company was active looked identical to the right one. Name the company on the connect surface and in the account picker, and say where the connection went when the callback lands under a different active company. Warn (bypassably) before authorizing a bank where the same user already holds live connections in other companies: several ASPSPs allow one active AIS session per login, so the new authorization can kill the others. The history start date already defaulted to the fiscal-year start; the card above it recommended a mid-year date and contradicted the selected option. It now states the fact and offers the shortcut without presenting it as advice. Not addressed: sharing one PSD2 session across companies. company_id is the tenancy anchor on bank_connections and cash_accounts hangs off (company_id, bank_connection_id), so that needs the session to become its own entity. See DECISIONS.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(supplier-invoices): show the posted line description in the voucher preview The "Verifikation som bokförs" preview built its expense debit lines with description set to the raw account number, so the BESKRIVNING column showed "5615" or "6990" where the posted verifikat actually says "Leverantörsfaktura 123, ACME AB". A hardcoded 11-entry ACCOUNT_LABELS map masked this for 2440/2641/26xx, which is why the column read as a mix of friendly labels and bare account numbers, neither of which was the posted text. The preview now renders exactly the line_description the engine writes: the shared invoice-level text on expense lines and 2440, "Ingående moms {rate}% {desc}" on 2641, and the reverse-charge pair taken straight from generateReverseChargeLines instead of being re-derived locally. buildSupplierDescription moves into its own dependency-free module so the client-side preview can call it without pulling the journal engine (and its Supabase server client) into the browser bundle. The account name stays reachable on the AccountNumber hover card. Picked option A from the issue, keeping the fixed invoice-level description rather than propagating each item's own text: the customer-invoice side already writes invoice-level descriptions, so per-item text would create an inconsistency between the two invoice sides rather than remove one, and it would need an aggregation-collision policy in the journal engine. Rationale recorded in DECISIONS.md. Refs #1258 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(bookkeeping): restore the copy icon on verifikat rows The row-language rewrite in #1123 reused the copy icon's slot for the new expand toggle, removing the zero-click copy affordance from the bookkeeping list without mentioning it. The leftover orphaned copy_voucher_tooltip key in both message files is what identifies it as collateral rather than a product decision. Restore a copy icon in the row's right-edge action cell, reusing that key for aria-label and title. stopPropagation keeps the click off the row's expand toggle. The icon is hover-revealed on md+ and always visible below it: #1123 collapsed the desktop table and the mobile card into one responsive table, so hover-only would leave touch users with nothing. Copy is no longer gated on posted. The copy_from handler and the GET journal-entries route never looked at status, so copying a draft already worked end-to-end and only the detail-page button hid it; the two list surfaces were already ungated. Both list affordances now respect canWrite, which previously dropped read-only users into a dialog they could not submit. The repo does not render components in tests, which is why #1123 removed this silently. Pin the source shape instead, the same way the copy-invoice query is pinned. Closes #1266 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(transactions): revalidate stale invoice match pointers before offering a match potential_invoice_id / potential_supplier_invoice_id are written once, at bank import, and never revisited. When one of several identical recurring invoices was settled by a different transaction, every other transaction kept pointing at the now fully paid invoice. The match dialog then measured the bank amount against a 0 kr remaining balance and reported a "Beloppen skiljer sig ... fakturan blir delbetald" partial payment, and the worklist offered the same dead suggestion as a one-click confirm row. Worse, the manual escape hatch was hidden exactly when it was needed: TransactionInboxCard only shows "Matcha mot leverantörsfaktura" when no suggestion exists, so a stale pointer left the user with no way at all to reach the correct invoice. Fixed by revalidating at read time rather than by clearing sibling pointers on settle. Invoices are settled through many paths (both match routes, mark-paid, MCP, bank reconciliation, SIE import), so write-time cleanup leaks the moment one is missed, while the candidate lookup covers every route into the list. The shared accept-lists in lib/invoices/matchable-statuses.ts mirror the CAS guards the match routes already enforce. - listSuggestedMatches and the transactions page candidate fetch filter on status + remaining_amount, so a settled candidate yields no suggestion and the manual picker reappears on its own. - InvoiceMatchDialog blocks a settled target with a distinct message and a disabled confirm. Not advisory: both routes reject it outright with MATCH_INVOICE_ALREADY_PAID / MATCH_SI_ALREADY_PAID, so no override could succeed. - The supplier detail card now shows remaining_amount like the customer branch, instead of total. On a partially paid invoice it used to print "1 250 kr" directly beside "Differens: 1 250 kr". - match-supplier-invoice clears potential_supplier_invoice_id on the transaction it just matched, mirroring the customer route. No bookkeeping was ever at risk: both routes already refused a settled target before creating a voucher. The damage was confined to a misleading dialog and a dead end. createQueuedMockSupabase gains passive call recording (calls / findCall / findCalls) because the proxy swallowed filter and update arguments, which made the new assertions inexpressible. Refs #1259, #1260 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(webhooks): dispatch on emit instead of waiting for the next cron tick (#1256) * feat(webhooks): dispatch on emit instead of waiting for the next cron tick The webhook dispatcher ran only on a per-minute cron, so the floor on delivery latency was up to 60 seconds plus the request. An external consumer that wanted to react as a transaction landed had only one alternative: polling /api/events, which the 100 rpm per-key limit makes expensive and which still cannot beat the tick interval. Schedules one dispatch cycle as soon as deliveries are enqueued. The cron is unchanged and remains the retry and sweep path; this only moves the first attempt forward. Wired into the event-bus fanout plus the two routes that enqueue a delivery directly: the :test verb, whose entire purpose is telling someone whether their receiver works, and the manual delivery retry. Three properties are load-bearing and covered by tests. The kick is never awaited, because eventBus.emit is awaited at ~99 call sites including journal_entry.committed and each delivery can burn a 10 s receiver timeout. It coalesces per function instance, so a bulk booking that emits once per row does not schedule one claim round trip per row. It claims 5 rows rather than the cron's 50, because it runs on the tail of a user-facing request. Double delivery is not a risk: claim_due_webhook_deliveries already claims FOR UPDATE SKIP LOCKED and flips rows to in_flight in the same statement, so a kick racing the cron sees disjoint rows. Does not close #1201, which asks for a realtime stream for API consumers. This is the cheap half. Refs #1201 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(webhooks): stop claiming the kick makes double delivery impossible Adversarial review of the previous commit caught an overstatement in its own comments. SKIP LOCKED keeps a kick and the cron from claiming the same row at the same moment, but claim_due_webhook_deliveries autocommits before any POST is issued, so from then on ownership is only status='in_flight' and a later cycle's recoverStuckInFlight sweep can re-arm a row still queued behind an earlier cycle's serial loop. Delivery is at-least-once, which is what the public docs already tell receivers ("the same delivery id may arrive more than once ... idempotency is on you"). The comments contradicted that. No behaviour change. The kick does not create this window: the cron claims 50 rows serially against the same 20 s stuck threshold, which is wider than what a batch of 5 can open. Refs #1201 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(bokslut): add bokslut-flow depreciation (78xx) back to the bolagsskatt base (#1253) * fix(bokslut): add bokslut-flow depreciation (78xx) back to the bolagsskatt base sumPostedYearEndDispositions reconstructs resultat fore skatt for the tax calculation, because generateIncomeStatement excludes every source_type='year_end' entry. It summed class 88 and 7533 but not 78xx, so planenlig avskrivning posted by the bokslut flow (lib/bokslut/assets/depreciation-engine.ts) was dropped from the income statement and never added back. The bolagsskatt base and the periodiseringsfond 25 % cap were therefore computed on an overstated result: tax too high by roughly 20.6 % of the depreciation. Also exclude the period's final bokslutsverifikation from the fetch. It carries source_type='year_end' as well and reverses every P&L account, 78xx/88xx/7533 included (verified against production closing entries), so once the year is closed it would cancel the add-back this function exists to produce. That hazard already applied to 88xx and 7533; the fix closes it for all three rather than widening it. Scope is deliberately the tax base only. Making the standalone resultatrakning show bokslut entries is a separate, larger change: the same exclusion is duplicated in the kpi_report_aggregates RPC, it moves displayed profit for every company that ran the bokslut flow, and it means removing the add-back at four call sites. Refs #1051 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(bokslut): scope the closing-entry lookup to the company and fail loudly Review (CodeRabbit + the compliance swarm, ASVS V8.2.1) flagged the new fiscal_periods read in sumPostedYearEndDispositions on two counts, both fair. It filtered only on the period id while every sibling query in the same function carries the tenant scope. Primary key or not, service-role paths have no RLS to fall back on and the repo's rule is to filter company_id explicitly, so it now does. It also discarded the query error. That mattered more than it looks: a failed read fell through to closingEntryId = null, which silently re-admits the closing verifikat's 78xx/88xx reversals and understates the tax base, i.e. exactly the failure this lookup was added to prevent. It now throws, and the surrounding catch turns it into the existing 'Failed to read posted dispositions' error. A wrong bolagsskatt is worse than a loud failure. Two regression tests: the lookup carries both eq filters, and a lookup failure propagates instead of degrading to a wrong number. Refs #1051 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(storage): drop the client-side DELETE policy on the documents bucket (#1254) * fix(storage): drop the client-side DELETE policy on the documents bucket 20240101000024 documents this bucket as WORM: "No UPDATE or DELETE policies". That described the repo, not production. Production carries a users_delete_own_documents policy that exists in no migration file: FOR DELETE TO authenticated USING (bucket_id = 'documents' AND (storage.foldername(name))[2] = auth.uid()::text) Under it, the uploading user can delete the storage bytes of any document they uploaded under the legacy documents/{userId}/... layout, using nothing but their normal browser token. That includes documents linked to a posted verifikat, which are rakenskapsinformation under the BFL 7 kap 2 § seven-year retention duty. deleteDocument()'s linked-check and the block_document_deletion() trigger both guard the document_attachments ROW, not the object: the row survives, still pointing at a file that is gone. Reproduced against a local replay of the full migration stream: with the policy present the uploader's own DELETE removes the object; with it dropped the same statement matches zero rows. Company-scoped keys were never exposed (their second path segment is the company id, not auth.uid()), so this only ever reached the legacy layout, which is where most documents still live. Safe because every in-app remove() on this bucket already runs on the service role, covered by service_role_all_documents. Deliberately narrow: users_read_own_documents and users_upload_own_documents stay. The Phase B backfill from 20260726092000 has not run, so dropping the legacy SELECT policy now would make existing documents unreadable. That is Phase C. The pg-real test asserts no DELETE and no UPDATE policy over the bucket under ANY name: the hole arrived under a name this repo never used, so pinning a name would not have caught it. Refs #1208 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(storage): make the WORM ratchet see FOR ALL and WITH CHECK policies Review caught two blind spots in the ratchet, both fair. It matched only polcmd 'd' and 'w', but polcmd '*' (FOR ALL) grants DELETE and UPDATE just as effectively, and FOR ALL is the shape the one legitimate policy on this table already uses, so a hostile one would look unremarkable in the catalogue. It also read only polqual, so an UPDATE policy carrying its bucket restriction in WITH CHECK was invisible. Both assertions now run through one helper that covers d/w/*, concatenates USING and WITH CHECK, and filters by grantee so service_role_all_documents (how the application does its authorized deletes) is excluded while every client-reachable role is not. A policy granted to PUBLIC has an empty polroles, which is the most permissive case there is, so it is treated as client-reachable rather than as "no roles". Matching on the substring rather than the exact `bucket_id = 'documents'` shape pg_get_expr emits today: a policy written as bucket_id::text or with the comparison reversed would slip past a stricter match, and for a WORM ratchet a false alarm is cheap while a silent hole is not. Adds a probe case that creates a FOR ALL policy and asserts the helper sees it, so the main assertion cannot pass vacuously. That case earned its keep immediately: it caught that node-postgres hands back a raw string for a name[] column, so the role filter needed rolname::text to work at all. Verified against a local replay of the full migration stream: red with the original prod FOR DELETE policy present, red with a FOR ALL probe, green without either. Full pg-real suite 933 passed. Refs #1208 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(storage): catch a destructive policy that names no bucket at all Adversarial review of the previous commit found the ratchet still failed open, and reproduced it: a policy with no bucket_id predicate covers EVERY bucket, documents included, so gating on the bucket name discarded exactly the widest hole. The concrete shape is Supabase's own stock "Enable delete for users based on user_id" template, USING (auth.uid() = owner), which is the single most likely form of a future dashboard edit. A destructive policy is now in scope unless it provably cannot reach this bucket, i.e. only a bucket_id predicate naming some other bucket exempts it. The behavioural assertions had the matching blind spot: fixtures were seeded without an owner, so an owner-based policy matched NULL and the DELETE reported 0 rows for the wrong reason. Objects now carry an owner the way storage-api stamps them in production, so those tests fail loudly instead of passing by accident. Two probes pin both directions: a bucketless policy must be reported (and is shown to really permit the delete), and a policy scoped to another bucket must not be, so the ratchet cannot start crying wolf on receipts or sie-files and get switched off. Verified against a local replay of the full migration stream: red with the stock bucketless template installed, green without it. Full pg-real suite 935 passed. Refs #1208 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(kontoplan): make a deactivated account reachable again (#1262) is_active=false read as "does not exist" on every read path but as "exists" on the (company_id, account_number) unique constraint, so a deactivated account vanished from the kontoplan with no way back and re-creating it answered "Kontonummer X finns redan i din kontoplan." The write side was already correct: POST /accounts/activate has a toReactivate branch and PUT /accounts/[number] accepts is_active:true. Both were simply unreachable, so this opens routes to them rather than relaxing the read filters, which are load-bearing for AccountsNotInChartError. - Kontoplan gets a "Visa inaktiva" filter; inactive rows carry an "Inaktiv" chip and the existing per-row switch reactivates them in one click. - Deactivating an account that has posted lines now warns first, using the usage count already loaded for the Verifikat column. - POST /accounts distinguishes the two collisions and returns the new ACCOUNT_EXISTS_INACTIVE code; AddAccountDialog offers "Aktivera kontot istallet" rather than a dead-end 409. The stored account is left exactly as it was; values typed into the failed create form are not applied. - bas-lookup consults the company's own chart before the static BAS reference, so a deactivated custom account reads as known and "Aktivera och bokfor" is no longer disabled for it. New in_chart / is_active fields let callers tell "will be added" from "will be revived". - BAS-katalog stops showing "Aktiverat" for an account the company holds but has deactivated; it falls through to a relabelled Aktivera button, and the per-class counts follow. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(supplier-invoices): flag foreign 0 % lines with reverse charge switched off (#1255) * fix(supplier-invoices): flag foreign 0 % lines with reverse charge switched off A foreign supplier charging no Swedish VAT is normally omvand skattskyldighet. With the reverse-charge switch off, createSupplierInvoiceRegistrationEntry emits neither the 26x4 output leg nor the 44xx/45xx basis lines, so ruta 20-24, 30-32 and 48 all stay empty and the momsdeklaration takes a shape Skatteverket rejects. For a fully deductible purchase the net moms att betala is unchanged, which is exactly why this goes unnoticed. The form already auto-ticks reverse charge for eu_business but not for non_eu_business, so that path slips through silently. Adds a pure helper plus a non-blocking banner cloned from the existing rc_account_warning block. Deliberately silent for swedish_business, where 0 % is a genuine exemption that belongs in no ruta at all, and phrased as a question rather than an assertion: a non-EU goods purchase cleared at customs is legitimately 0 % without reverse charge, and pushing that user into ticking the switch would manufacture a new wrong verifikat. Does not add the exempt/import/other picker the issue proposes: supplier_invoices.vat_treatment is metadata that no booking or ruta mapping reads, and the codebase cannot book import VAT at all, so an import option would imply ruta 50/60 were handled when they are not. Refs #1042 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(supplier-invoices): name the local-VAT case in the foreign 0 % hint Review flagged that the most common foreign document a Swedish small company sees is an invoice carrying the supplier's OWN local VAT, booked at 0 % Swedish VAT with reverse charge correctly off. The banner fires there, and the previous copy only offered "momsfri av annat skal, till exempel en varuimport" as the way out, which does not describe that invoice at all: it is not VAT-free, it carries foreign VAT. Names both legitimate cases explicitly and says 0 % is correct in them, so the hint cannot read as an instruction to tick reverse charge on a purchase where that would produce a wrong verifikat. Title also narrowed to "utan svensk moms" for the same reason. Refs #1042 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(sandbox): call the sandbox assistant Assistenten, not Anna (#1244) A named persona earns its name once someone has been through onboarding and chosen it: it is their assistant and they named it. Nobody in the sandbox chose anything, so a first name reads as a character the product invented and implies a relationship the visitor never opted into. Both halves move together, which is the point. profile_summary is the agent's own self-description inside the system prompt, so leaving it as "Du är Anna" would have the header say one thing while the assistant introduces itself as another in its first sentence. Nothing else in the stack checks that pairing, so a test now does. Scope: this changes the seed, so new sandbox companies get the new name. The 483 sandbox profiles already seeded keep 'Anna' (the seeder returns early once a profile exists, and its caller only runs while verified_at is null). Backfilling those is a production write on demo data and is being raised separately rather than smuggled into a code change. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * feat(reports): show the last posted voucher per series in report headers Adds a "Senaste bokforda verifikat: A 214, B 37" line to the balans- and resultatrapport, so a printed or exported report answers which vouchers are actually in it rather than only which dates it spans (#1267). Reads MAX(voucher_number) over posted entries, never voucher_sequences.last_number. The sequence counter is an allocation high-water mark that drifts from the books in both directions: next_voucher_number burns a number when the follow-up insert fails, delete_last_voucher decrements by one instead of resetting to the new MAX, and pre-RPC SIE imports left it behind. Since the point of the line is avstamning, an allocated number would send a reconciler chasing a gap that does not exist, so the label says plainly that the number is the posted one. Scoped to the report own date range, so a Q1 report printed in November says something true about Q1. The balansrapport keeps the fiscal-year start as its lower bound because it accumulates. Skipped on a dimension-filtered resultatrapport: that report already discloses it is partial, and an unfiltered voucher range beside a filtered result invites the wrong conclusion. Populated in both engines, so the JSON, PDF and XLSX routes all inherit it without signature changes. Best-effort: a header nicety never breaks a report. The pure formatter lives in its own module so the client view does not pull the Supabase query path into the browser bundle. No new i18n keys; both report views and the PDF template are hard-coded Swedish per the "stays Swedish" report surfaces in .claude/rules/i18n.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(customers): stop rendering personnummer ciphertext, make unreadable rows editable, add a reveal path (#1263) customers.personal_number holds AES-256-GCM ciphertext (20260726110000). Three defects compounded into one broken surface for private customers. The list queried Supabase from the browser with select('*') and rendered the raw value, 76-82 chars of hex, into the nowrap identifier cell. It now reads GET /api/customers, which already masks every row, so the ciphertext never leaves the server. Searching by personnummer works again: the client filter had been matching against ciphertext and could never hit. A row whose value cannot be decrypted renders as the placeholder '********-????'. None of the three mask checks recognised it, each having its own '-1234'-only copy, so such a customer could not be edited in ANY field: name and address edits 400'd on a personnummer the user had no way to correct. All three now share one pattern from the new crypto-free lib/customers/mask-personal-number.ts, which the client form can import. Typing a fresh personnummer overwrites the unreadable value, which is the only repair possible: the rejected writes failed whole INSERTs, so there is nothing to backfill. The value was write-only by construction. GET /api/customers/{id}/personal-number is the deliberate drill-in, mirroring the employee convention, gated on the write role because .compliance/ropa.yaml listed no_full_value_read_endpoint as a safeguard for this column; that entry is rewritten rather than left stale, and reveals log actor and customer id but never the value. Also: arcim-migration wrote the identity number as plaintext, which aborts any import containing a Privatperson with 23514 since the constraint flip; and the customer embeds on /api/invoices shipped ciphertext to the browser on every invoice read. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: enhance ruta 05 handling for dynamic revenue accounts - Introduced `fetchDynamicRuta05Accounts` to fetch company-specific revenue accounts marked with a VAT rate, addressing issue #1261. - Updated VAT declaration logic to include these dynamic accounts in ruta 05 calculations, ensuring accurate reporting for user-added accounts. - Modified `ACCOUNT_RUTA` to include account 3000 for completeness in ruta 05. - Enhanced tests to validate the inclusion of user-added revenue accounts in ruta 05 and ensure correct VAT calculations. - Seeded default VAT rates for BAS revenue accounts to ensure proper classification in the VAT declaration. * fix: enhance data handling and masking in customer and invoice APIs * fix(vat): resolve the 3000 gruppkonto's rate for the ruta 05 base split 3000 "Forsaljning inom Sverige" is mapped to ruta05 by ACCOUNT_RUTA, so a balance on it is filed in the right box already. What was missing is the rate split: unlike 3001/3002/3003 the account number carries no sats, and fetchDynamicRuta05Accounts skipped it because it is in ACCOUNT_TO_BOX. A company posting to the gruppkonto therefore got a ruta 05 total that breakdown.invoices.base25/12/6 did not add up to. Surface those rates separately as staticRateByAccount: rate-only on purpose, because the static map already sums the account and adding it to the dynamic account list would double the filed figure. A test pins that single-count property. Also add 3000 to the MCP server's RUTA_05_ACCOUNTS, which is the display list behind report.rutor.ruta05: without it a 3000 balance appeared in the filed projection but not in the report the agent reads back. The comment claiming SALES_OUTPUT_VAT_SHORTFALL reads base25/12/6 was wrong and is corrected. That check derives its expected base from the output-VAT rutor (ruta10/0.25 + ruta11/0.12 + ruta12/0.06); nothing reads the per-rate bases, which are reporting metadata. So the incomplete split never affected a filed return or a warning, only the breakdown. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Jakob Wennberg <149234542+jakobwennberg@users.noreply.github.com> |
||
|
|
f24b26a139 |
fix: similar-sweep currency remediation, security hardening and v1 API fixes (#1215)
* fix(security): gate replace_sie_import behind owner/admin membership The RPC was SECURITY DEFINER with EXECUTE granted to PUBLIC and anon, no company_members lookup, no auth.uid() reference and no unauthorized raise, while setting gnubok.allow_delete to disarm the BFL immutability and retention triggers. Any caller holding a company_id and an import id could hard delete another tenant's verifikationer. Confirmed live in production. Applies the same fail closed owner/admin guard that undo_sie_import already carries (migration 20260624120000), resolving the actor from COALESCE(p_user_id, auth.uid()) so it denies when the role is NULL, then revokes EXECUTE from PUBLIC and anon. search_path and the raised statement_timeout are restated, since CREATE OR REPLACE drops settings that are not repeated. userId is a required parameter on replaceSIEImport: the service client has a NULL auth.uid(), so a caller without an explicit actor now fails to compile rather than hitting the closed gate at runtime. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(security): validate arcim OAuth callback state server side The callback route is skipAuth and decoded the state parameter as plain base64url JSON, trusting consentId and provider from it. A one time code was minted at flow start and never read. An unauthenticated attacker who learned a consent id could run an OAuth flow on their own provider account and post the callback with a forged state, landing their tokens on another tenant's consent, so the victim's next migration imported the attacker's ledger. State is now an opaque randomBytes(32) pointer to a provider_otc row, consumed by a single atomic UPDATE guarded on used_at IS NULL and expires_at, so a replay loses the row lock race and updates nothing. provider is read from provider_consents rather than trusted from the client. provider_otc already existed for exactly this purpose and was never wired up. Also scopes getConsent to an owning company, closing a cross tenant status oracle where the preview and migrate paths echoed a consent's status before the scoped check ran. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(security): scope documents storage to company_id (phase A) The documents bucket policies matched on auth.uid(), and upload keys were documents/{userId}/..., so company membership was never consulted. Removing a member revoked nothing: their session still authenticated and they kept direct Storage read access to every receipt, supplier invoice and bank statement they had uploaded. The same bug was fixed for sie-files in 20260416120000; this bucket was left behind. Phase A is additive. Company scoped policies are added alongside the uploader scoped ones, uploads move to documents/{companyId}/{userId}/..., and reads accept either layout so nothing breaks mid migration. Phase C, which drops the old policies, is gated on the backfill reporting zero remaining legacy prefix objects. The policy compares the company segment as text rather than casting to uuid the way sie-files does: this bucket holds keys whose second segment is not a uuid (MCP audit packages), and Postgres does not guarantee the bucket prefix qual runs before the cast, so a planner reordering would raise 22P02 and fail the whole query instead of filtering the row out. deleteDocument now removes both candidate keys. Removing only the stored pointer would leave a readable orphan copy of a document the user asked to erase. The backfill script is included but has never been run. It defaults to dry run, refuses .env.local by name, and verifies each copy is readable and SHA-256 identical before repointing the row. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(security): enforce events:read scope and membership on /api/events This was the only one of the three validateApiKey call sites with no downstream guard: v1 and the MCP server both check scope and re-verify company membership, this route did neither. An events:read scope existed and was documented as gating the endpoint but was never called, so a legacy key falling back to DEFAULT_SCOPES read the full log. The bound company id went straight from the api_keys row into a service role query, so a key whose user had been removed from the company kept reading. Adds the scope check before any database access, re-verifies company_members with archived_at IS NULL, honours test mode by stamping X-Gnubok-Mode instead of ignoring it, applies minimisePayload so the pull surface can never return a wider payload than the push surface, and replaces the three flat error strings with the canonical envelope. Test key reads are served rather than blocked: TEST_KEY_WRITE_BLOCKED is gated on mutations in with-api-v1, so a read gets the same treatment as every other v1 read endpoint. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * perf(bookkeeping): sweep remaining journal_entries!inner embeds A previous refactor removed this pattern from lib/reports and introduced fetchEntryLines, but the class was never swept. Seventeen sites remained and had become the top application consumer of production database time: measured across the resulting query shapes, 32,694 calls and 25,848 seconds of execution, mean 790ms, with shapes averaging 2.6s and 3.0s and maxing at 7,962ms against the 8s statement_timeout, which surfaced to users as 500s on the booking path. PostgREST compiles an embed with filters on the embedded side into a correlated INNER JOIN LATERAL with a parameterized LIMIT, which stops Postgres reordering the join, so each query walked the whole journal_entry_lines table across all tenants. Driving from the entries side instead turns that into two indexed round trips. Converted sites keep their existing shape: the helper reattaches the parent entry under the same key the embed produced. Several conversions also remove a latent silent truncation where an unpaginated query was capped at PostgREST's 1000 row ceiling. Two deliberate exceptions. The free text ilike legs of the MCP display query stay on the embed, because each is capped at legLimit and that cap drives the truncation contract the tool reports, while the helper is unbounded. The accounts route moves to the existing get_account_usage_counts RPC instead, since its embed was a head count and the helper returns rows. commitEntry's write path is untouched: the change there is confined to the read query of the pre-commit dimension rule check. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(api): anchor v1 list cursors on created_at Page two returned page one, forever, while still advertising a fresh next_cursor. The three routes sorted by and encoded a Postgres date column, which serializes as YYYY-MM-DD, but decodeDefaultCursor validates the cursor timestamp as full ISO-8601 and returned null, so the keyset filter was never applied and has_more never went false. An integrator syncing verifikat looped on the newest rows indefinitely. The transactions route already solved this and its comment names the trap; the fix was never ported. All three now order and encode on created_at with an id tie break, matching the transactions keyset predicate exactly. ISO_TIMESTAMP is deliberately left alone: relaxing it would silently change sort semantics on the route that currently works. Default ordering therefore moves from business date to insert order. Every business date is still on the row, and the invoices list gains date_from and date_to filters so a date range is still reachable; the other two already had them. The tests use an in-memory PostgREST that actually evaluates the filters, because the repo's pass-through mock cannot catch this class of bug: the bug is that the filter is never sent. They walk to exhaustion with a hard iteration cap, so an unterminated walk fails instead of hanging. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(api): separate dry run from commit in the idempotency hash The request hash was built from url.pathname, which excludes the query string, so a dry run and its commit hashed identically. Following the flow documented in dry-run.ts, re-issuing the request with the same Idempotency-Key returned the cached preview with Idempotent-Replayed set and wrote nothing, while reporting 200. An agent or integrator saw success for a write that never happened. dry_run is folded into the hash only when true, not as an unconditional boolean. Including it as false would change the hash of every ordinary write, and with a 24h idempotency TTL any key in flight across the deploy would fail the request_hash comparison and 409 on a legitimate retry. Both hash call sites now go through one shared helper so they cannot drift into a permanent cache miss, and dry run responses are no longer stored at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * ci: install the Bedrock SDK out of tree in the compliance review The Swedish accounting compliance gate had failed ten consecutive runs and so was posting nothing. With --no-package-lock npm discarded the lockfile and re-resolved the whole tree from package.json, floating @hookform/resolvers to 5.4.3, whose valibot ^1 peer conflicts with the pinned valibot 0.39.0. Installing into the parent of the checkout resolves only that one package, so an unrelated peer conflict can never take the gate down again. Node still finds it because ESM bare specifiers walk up parent node_modules; NODE_PATH would not have worked, as it is CommonJS only. --legacy-peer-deps was rejected because it masks future genuine peer conflicts and still reifies the full tree. The same step's SDK version is aligned from 0.31.0 back to the 0.29.1 that package.json and check:guards enforce after the streaming outage. That drift went unnoticed because the pin guard only inspects package.json and the lockfile, never workflow files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * build(docker): generate crontabs from vercel.json vercel.json defines 16 cron jobs; both Docker crontabs carried 9, and were byte identical to each other. Self hosted deployments therefore never sent recurring invoices, never dispatched webhooks and never cleaned up idempotency keys. tax-deadlines also ran once a year on 2 January instead of daily, and documents/verify weekly instead of daily. Extension crons are included rather than excluded. The Dockerfile copies the whole tree before building, so every extension cron route is compiled into the image regardless of the enabled preset, and each returns 200 when its extension is unconfigured, so curl -sf logs no failure. Two such entries were already present in the crontab for extensions absent from the preset, which settles the intent. documents/verify is treated as drift rather than a self hosted concession: the weekly cadence was present in the hosted crontab too, and the run is capped at 200 documents walking a nulls-first queue, so weekly drains the integrity queue seven times slower on a check that exists for BFL retention. webhooks/dispatch keeps its per minute cadence, adding 1,440 requests a day on self hosted. A gentler tick would silently stretch the first retry, since the retry ladder opens at 60 seconds. SCHEDULE_OVERRIDES is the one line place to change that. A parity test asserts the path sets match minus a documented exclusion list, and ratchets three cron routes that are currently scheduled nowhere so they are named rather than silently rotting. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore(observability): add a provider agnostic error sink There is no error tracking in this codebase: logs go to console and Vercel retention and nowhere else, nothing alerts on the 16 cron jobs, and seven code comments across lib, app, components and extensions asserted that Sentry captures errors when Sentry is not a dependency. The two most recent bug fixes on this repo were both discovered by customer email. This adds the sink, not a vendor. No dependency is taken: the interface has a no-op default and a registration point, so behaviour is unchanged until an adapter is registered. Releases are tagged from the build id already inlined by next.config.ts. Redaction moved out of lib/logger.ts into a leaf module that both the logger and the sink import, so there is one denylist and no path from application data to a third party can skip the personnummer regex, including direct sink calls that bypass the logger. That matters here because these logs carry personnummer and financial data. verifyCronSecret now reports its own 401s, which covers all 16 jobs without touching a route file and catches the case where CRON_SECRET is rotated without updating the scheduler and every job silently 401s forever. The threshold is one failure rather than the backup alert's three: suppressing the first occurrence is precisely how an outage stays invisible. The seven misleading comments are corrected to describe what the code actually does, including the two cases that still are not covered: the client side one, since the sink is server side, and a warn level call that is not forwarded. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: remediate the 2026-07-26 similar-sweep findings across all surfaces Resolves the ~150-finding sweep (dev_docs/similar-sweep-2026-07-26.md) with one agent per finding; every behavioural fix carries a regression test proven to fail at HEAD. Full status, corrections to the sweep, refusals and open decisions in dev_docs/similar-sweep-2026-07-26-remediation-status.md. Structural roots closed: - resolveSekAmountOrNull(): honest SEK resolution refuses instead of booking 1:1; four duplicated toSek closures now refuse via INVOICE_FX_RATE_MISSING - ledger-line-amount.ts: journal_entry_lines.currency labels the document, not the amount; SQL pre-filter decoy proven and fixed - sparse-patch.ts: .partial() does not strip .default() in Zod 4.4.3; the exploitable salary payslip-line PATCH and KPI preferences sinks fixed - tests/schema: migration-replay phantom-column guard (13k+ refs, closed CHECK sets, onConflict targets); found 28 real defects, all fixed, all four baselines now empty - three new ratchet guards: sek-labelled-amount, cross-extension-import, ungated-extension-route Highlights: lawful VAT-rate set on all seven invoice surfaces (ML 6 kap), RC input VAT mismatch wired on web + both MCP callers, missing-underlag resource delegates to the shared RPC predicate, push-notifications consent polarity fail-closed, deadlines undo honours requested state, silent-failure and read-side-fabrication classes fixed across settings/KPI/inbox/Stripe/ Arcim/kassaflodesanalys, error-envelope stringification fixed at 10+ sites with isSwedishUserMessage extended. Also includes the parallel session's MCP invoice tools (update_invoice, recurring schedules, invoice deliveries) which share files with the sweep work and are verified green together. 13 new migrations are NOT applied anywhere; they apply via branch merge. 20260726120000 backfills 1247 supplier-invoice rows. pg tests for new DDL are written but unrun (no local Postgres). Verified: 11088 tests / 881 files green, tsc 0 non-test errors, lint 0 errors, check:guards passing, MCP payload 57475/57500. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(migrations): rename replace_sie_import migration off main's 20260726090000 version origin/main shipped 20260726090000_agent_quota_rpc_caller_guard.sql; keeping our replace_sie_import migration on the same version would abort the Supabase apply with a schema_migrations_pkey duplicate at merge time. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(review): remediate pre-publish deep-review findings across all slices A 13-agent review of the full branch diff surfaced 1 critical, 5 high and ~45 further findings; this commit resolves them in one pass: - replace_sie_import / undo_sie_import: p_user_id honored only for service_role callers; any other caller is pinned to auth.uid() (impersonation gate bypass), authz raise errcode 42501 mapped to a Swedish 403 in the route, new caller-guard migration for undo - bulk_book_transactions refuses homogeneous non-SEK batches instead of writing foreign magnitudes into SEK ledger columns - credit-note cap trigger: company-match on credited_invoice_id, no cross-tenant figures in exception text - link_voucher RPCs resolve NULL invoice currency as SEK end to end - personal-number ciphertext CHECK split into NOT VALID + VALIDATE - same-currency foreign settlements clear 1510 at booking rate and book realized diff to 3960/7960; rate-less foreign write paths refuse - receivables revaluation covers partially_paid and outstanding amounts - period lock guard paginates candidates past the PostgREST 1000 cap - documents: service-client storage removals after authz, dual-layout reads in integrity cron and archive export, backfill delete-source sweep actually deletes with hash verification and shared-key grouping - invoice matching normalizes NULL/lowercase currencies (regression), duplicate candidates stop claiming amount matches they never ran - match-invoice aborts on any booking failure (no paid-without-verifikat) - refresh-exchange-rate reverts on concurrent booking (TOCTOU window) - KPI preferences upsert arbiter aligned to the company-scoped constraint - personnummer_last4 stripped from all salary responses incl. MCP tools - worked-hours batch restores destroyed rows on conflict and error paths - MCP: shared duplicate-claim builder (no more 'null kr'), short-circuit on tag_journal_lines overflow, auto_send schedules stage as high risk - observability sink redacts emails/IBANs/API keys and keeps redacted stacks in prod; assorted small guards (safe-return-to /@, dry_run=True, cursor helper off-by-one, OAuth state TTL 10 min, arcim saveMappings call removed) Full dispositions, deferred items and hand-verified accounting numbers are documented in the PR body and DECISIONS.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(personnummer): implement masking and encryption for personal numbers with tests * fix(review): address CI and compliance-bot findings for PR #1215 pg-real: the CI image's auth shim reads the legacy request.jwt.claim.role GUC, so both service-role simulations (runAsServiceRole and the invoice-delivery test's local helper) never satisfied auth.role() = 'service_role' and every legitimate p_user_id path failed closed; the shared helper now sets both GUC shapes plus SET LOCAL ROLE with a fail-loud sanity check, and the delivery test reuses it. The link-voucher migration had recreated both RPCs from pre-rewrite file text, reintroducing the NULL-unsafe membership pattern the null-safe-tenant-guards ratchet bans; both guards now use public.caller_is_company_member() with all currency changes preserved. Compliance bots: the customers export now emits the standard masked form instead of raw AES-256-GCM ciphertext in the Org-/personnummer column, and maskCustomerRow returns a non-round-trippable placeholder on decrypt failure instead of 500ing the list. MCP parity: gnubok_lock_period's staging pre-check now runs the exact countUnbookedInPeriod the commit path enforces (exported from period-service; local mirror deleted), and gnubok_agi_status resolves AGI state run-scoped so a correction run no longer renders as already filed. Declined with evidence: PR-Agent's opening-balances null-zeroing concern (all mergeable columns are NOT NULL with defaults per 20260713101000). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(review): address codex review findings on PR #1215 - restore 20260726140000 to its preview-recorded content and restate the NULL-safe tenant guard under 20260727130000: a recorded migration version never re-runs, so the in-place edit could not reach the preview branch - replace toFixed() with sv-SE two-decimal formatting in the ROT/RUT cap warning texts and update the pinned test expectations - drop the em dash in the fiscal-periods route comment - strip trailing whitespace in import-existing.test.ts Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(reports): raise timeout on real PDF render tests renderToBuffer does real @react-pdf layout work and exceeds the 5s default when the full suite saturates the CPU; tests pass in isolation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
36a1df4f6b |
feat(import): detect and import the article register's Valuta column (#1183)
Fixes #1167. The register export gained a Valuta column in #1166 but the importer ignored it, so re-imported non-SEK articles silently became SEK, breaking the export -> edit -> re-import round-trip. - Column detector recognizes valuta/valutakod/currency (claimed before generic columns; no keyword collision with Momskod). - Parser normalizes to upper-case ISO shape, drops malformed codes with a file-level warning, and carries currency per row. - Execute route validates codes lazily against the currencies table (FK stays the backstop when the reference read fails), imports valid codes, defaults absent to SEK, and in merge mode only overwrites when the file explicitly carries a valid currency. - Edit step shows a muted currency marker next to non-SEK prices; manual column mapping offers Valuta. - Export docblock caveat removed. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d840257c0c |
Add/stripe connect transactions (#1139)
* fix(mcp-oauth): allow ChatGPT connector callbacks and resume OAuth after login Add chatgpt.com/connector/oauth/* (per-instance) and the legacy chatgpt.com/connector_platform_oauth_redirect to the built-in OAuth redirect allowlist so ChatGPT MCP connectors can register and authorize. Fix the login page dropping the ?next= destination: an OAuth-initiated visit that required login previously ended on the dashboard and the connection flow silently died. Login now resumes to the sanitized next path (hard navigation, since the consent page is route-handler HTML), carries it through the MFA step-up as returnTo, and /mfa/verify hard-navigates for /api/ destinations. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(transactions): dedup incoming feed rows against booked hand-entered twins Users who bookkeep via MCP/chat first and connect their bank afterwards got the same movement twice: the synced row's external_id lives in a different namespace, the free-form manual title never text-bridges the bank's raw string, and the cross-channel mirror deliberately excluded manual/mcp rows. Extend the mirror with a booked-hand-entered track: an incoming feed row is skipped when a BOOKED manual/mcp row shares its (date, ore) bucket count- symmetrically. Gates beyond the feed-vs-feed mirror: stored row must be booked (staged rows never consume an import), currencies must not contradict (bucket key is date+ore only), the cash-account guard applies to the count exactly as to consumption, and symmetry uses the Layer-1-unmatched incoming count so an already-stored row cannot inflate it. Consumption stamps the batch cash_account_id onto an account-unbound hand row, so one hand row can never consume feed rows on other accounts in later syncs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(bookkeeping): inline verifikat rattelse (strike lines + text/date edit) Second sanctioned correction track under BFL 5 kap 5/9 pp, Fortnox-style: strike lines inside a posted verifikat with replacements in the same voucher, and correct description/entry_date without an andringsverifikat. Envelope: posted entries, open unlocked periods, company lock date, same-period date moves, structural/FX/doc-linked lines excluded, and a reconciliation guard preserving per-account net on bank/reskontra sides of externally linked entries. Every rattelse writes an immutable who/when row (journal_entry_rattelse_log, WORM, archived as rakenskapsinformation) and struck originals render struck-through in the verifikat; list rows and the detail header carry a Rattad marker. CLAUDE.md hard rule 1 and the swedish-accounting-compliance skill are amended to state the two-track rule. Staging carries the DDL; prod gets it on merge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: live saldo in booking form, prior-year window comparison, hideable assistant FAB - Manual journal entry: saldo column now shows before -> after computed from the typed debit/credit amounts (direction feedback while booking) - Resultatrapport: a narrowed date range now compares against the same window shifted one year back (#862), merged across fiscal periods for brutet rakenskapsar; P&L rows report window activity instead of rolled-forward YTD closing - Assistant FAB: per-user hide toggle (user_preferences.hide_assistant_fab, settings > assistant), sidebar entry unaffected; collapsed sessions keep their reopen handle Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(stripe): sync balance transactions as a bank feed on 1686 Import the connected Stripe balance into the transactions inbox, opt-in per connection (transaction_sync_enabled on stripe_connections): - Balance transactions map to feed rows with the two-row gross+fee split and frozen external_id formats (stripe_{acct}_{txn} / _fee), dated on created, bound to a provisioned "Stripe-saldo" cash account on 1686 so booking settles against the clearing account by construction. - Double-booking protection: settled payment-link charges import pre-linked to their settlement entry; payout rows import pre-linked to the payout entry; processPayoutPaidEvent claims the payout's fee rows at booking time (linkPayoutFeedRows, idempotent from both directions). - Cursor last_balance_txn_synced_at with 24h overlap; first run backfills 90 days floored at the day after the company lock date. - Nightly cron /api/extensions/stripe/transactions/cron (03:30), transaction-sync toggle route, "Synka nu" covers both feeds, settings panel toggle with last-synced/backfill note, sv+en strings. - Migration 20260723200000 (applied to staging). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(transactions): offer match-to-voucher on unbooked history rows Unbooked transactions with is_business already set (e.g. left behind when a voucher was removed without a full uncategorize) land in the history list instead of the inbox, where the match-against-existing-voucher action did not exist, leaving them with no path back to voucher matching. Add the same menu item to the history list for unbooked rows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(transactions): enhance ownership checks and error handling in journal entry routes --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
43f7ccab9e |
feat(invoices): allow BAS class 1-3 posting-account overrides and complete the aktiekapital note (#1121)
- invoice/article posting-account overrides accept active class 1-3 accounts; class 1-2 (balance-sheet) accounts are rejected on VAT-bearing lines so the ruta 05 tax base always books to a 3xxx account - shared posting-account regex across server schemas, pending-operation re-validation, and client forms - share-capital settings (aktiekapital/antal_aktier) feed the annual-report note; kvotvarde derived per ABL 1 kap 6 $; all-or-nothing pair constraint - signed per-rate VAT breakdown on credit-note PDFs; U+2212 to ASCII hyphen Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b0044bfe98 |
fix(arsredovisning): make the aktiekapital note completable via compa… (#1118)
* fix(arsredovisning): make the aktiekapital note completable via company settings The annual report warned every AB that the aktiekapital note was missing and pointed at Installningar -> Foretag, but the referenced columns (aktiekapital, antal_aktier, kvotvarde) never existed and no settings UI was ever built, so the warning was a dead end and no AB could produce a complete note before Bolagsverket filing. - migration 20260723103000: company_settings.aktiekapital (numeric) and antal_aktier (integer) with positive CHECKs; kvotvarde is intentionally not stored since ABL 1 kap 6 defines it as aktiekapital / antal aktier - build-data.ts (K2 and K3 note paths): select only the two stored columns and derive kvotvarde with roundOre - UpdateSettingsSchema: aktiekapital (positive), antal_aktier (positive integer), both nullable to allow clearing - new ShareCapitalForm section on Installningar -> Foretag, rendered for aktiebolag only, with live derived kvotvarde display; wired through the existing CompanySettingsContent save path (empty string clears to null) - sv/en strings; settings route tests (round-trip, clear, 400 on invalid); builder tests for derived kvotvarde and the empty-settings warning Staging (metjnjrhvujscngnpzdv) already has the columns applied and the note verified end-to-end against a rehearsal company. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(arsredovisning): address PR review findings on the share-capital note - enforce aktiekapital/antal_aktier as an all-or-nothing pair (DB CHECK, K2/K3 note guard now requires both, partial pair warns instead) - numeric(15,2) column, .int() Zod constraint, maxFractionDigits 0 render - guard numberOrNull against NaN; align kvotvarde preview with schema - strengthen clearing test, add fractional and partial-pair tests Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
466e55a015 |
Fix/invoice delivery and payment accounts (#1116)
* fix: reconcile annual reports with final closing entries * test: cover annual report depreciation and VAT balances * Merge remote-tracking branch 'origin/main' into fix/usr-fdbck-ch * fix: show exact invoice delivery details * fix: use currency account in invoice emails * fix: address invoice delivery review feedback * fix: harden invoice delivery and payment accounts * test: assert RLS-denied zero-row updates * fix: close remaining invoice compliance gaps * fix: harden invoice archive authorization * fix: close invoice delivery review findings * fix: verify delivery finalization results * fix: cap combined invoice email recipients * fix: close final invoice compliance findings * fix: prevent stale payment account saves * test: prove invoice delivery isolation * fix: close invoice privacy review findings * test: normalize delivery retention dates |
||
|
|
321e684523 |
Fix/usr fdbck ch (#1105)
* fix(privacy): mask voucher amounts in session replays * fix: persist transaction source filter * fix: clarify invoice filenames and booking previews * fix: truncate long uploaded filenames * feat: add invoice delivery history * fix: harden invoice delivery history * fix: include invoice deliveries in full archive |
||
|
|
3e1ea29d02 |
fix(pending-ops): record posted ids and land failed_partial instead of clean rejected after partial commits (#842) (#1110)
Multi-step executors (match_transaction_invoice, credit_invoice) post an irreversible voucher or persist a credit note and then run later fallible steps. A failure there previously marked the whole op status=rejected, hiding the posted entity and its id from operators. - new migration 20260722134114: add failed_partial to the pending_operations status CHECK and treat it as terminal in both immutability triggers (immutable, undeletable, never re-claimable) - PartialCommitError + ExecutorResult.partialPostedIds carry the posted ids; the dispatcher writes status=failed_partial with result_data.posted_ids and returns code=partial_commit - instrument only the two named executors; hoist the read-only settlement-account resolution above the storno in the match executor - consumer sweep: status union + query schema widened, failed_partial folds into the Avvisade tab with a badge and posted-ids detail line, bulk/reject routes and MCP tools message it explicitly, worklist and expiry sweep intentionally untouched (not pending work) - tests: pg-real coverage for the new terminal semantics, dispatcher unit tests for both partial paths plus byte-for-byte regression guards Fixes #842 Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
8294899543 |
test(skatteverket): guard LEGACY_DISCOVERY_HOSTS against config drift (#1093) (#1107)
Add validateLegacyDiscoveryHosts() next to the allowlist in lib/api/v1/base-url.ts and a unit test that pins the registered production configuration (app.accounted.se canonical, app.gnubok.se SKV OAuth pin). The invariant is checked both ways: the NEXT_PUBLIC_SKV_OAUTH_BASE_URL host must be reflectable by discovery (canonical or allowlisted), and every allowlist member must be accounted for by the registered configuration. Drift is now a red CI test instead of a silent production re-auth failure near a filing deadline. No runtime behavior changes. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
e11f70b347 |
Bug/gh issues fiz (#1103)
* refactor: optimize page loading and data fetching * fix: resolve recurring production runtime errors * feat: add MCP company and customer updates * fix: handle year-end tax adjustments * feat: harden annual report compliance * fix: expand invoice logo and font support * fix: sanitize API route error responses * fix: sanitize user-facing error messages * feat: persist onboarding and tax assessment notices * fix: reduce cloud backup audit churn * feat: refine invoice editor layout * fix: show saved tax adjustments in INK2 * fix: complete annual report API mappings * docs: record operational safeguards and decisions * fix: harden annual report review findings * fix: adjust column span for description based on VAT registration * New css class name |
||
|
|
b420f3e1d9 |
feat(domains): dual-domain cutover to app.accounted.se (#1087)
* feat(domains): dual-domain cutover to app.accounted.se The user-facing app moves to app.accounted.se while app.gnubok.se stays alive for machine traffic (MCP connectors, API keys, third-party OAuth callbacks, webhooks, crons), so no third-party callback registration is on the critical path. - next.config: host redirect app.gnubok.se -> NEXT_PUBLIC_APP_URL for page traffic only (/api, /.well-known, /_next excluded). Arms itself only once NEXT_PUBLIC_APP_URL leaves the legacy host, so merging this is inert and the cutover is a pure env flip + redeploy. - skatteverket: redirect_uri pinned via NEXT_PUBLIC_SKV_OAUTH_BASE_URL (Utvecklarportalen registration is slow to change); the OAuth callback now resolves the flow from the state token + stored oauth_user_id via the service client instead of session cookies, which no longer exist on the OAuth host. Legacy same-domain flows fall back to the session. - popup listeners (SkatteverketConnectPanel, AGIPanel) accept postMessage from the pinned OAuth origin; event.source identity check unchanged. - /.well-known discovery docs reflect the allowlisted request host so existing MCP connectors on app.gnubok.se keep a self-consistent issuer/resource after the flip; spoofed hosts fall back to canonical. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: log dual-domain cutover decision Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(review): recency-bound SKV state lookup, exact localhost match in discovery allowlist - The oauth_state lookup now only considers rows updated in the last 10 minutes: bounds how long a leaked/phished authorize URL stays completable, keeps the row set far below PostgREST's 1000-row cap, and surfaces query errors instead of misreporting them as CSRF. - resolveDiscoveryBaseUrl matches localhost/127.0.0.1 exactly; the prefix check reflected spoofed hosts like localhost.evil.example. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d860567976 |
feat(pending): bulk reject selected operations in granskning (#1085)
* feat(pending): bulk reject selected operations in granskning The granskning queue could approve selected operations in bulk but rejection was one row at a time. Adds: - POST /api/pending-operations/bulk-reject: one guarded UPDATE (status='pending' filter) so rows resolved in a parallel session are reported as skipped instead of being flipped; optional rejection_category/rejection_reason applied to every rejected row so agents still learn from bulk 'no'. No high-risk skip server-side: rejecting posts nothing to the ledger. - 'Avvisa valda' button next to 'Godkänn valda'; the existing reject dialog doubles as bulk confirmation (category + note apply to all). - Route tests: 401/403/400/500, not-found, already-handled skip, read-write race, happy path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(pending): disable both bulk buttons while either bulk action is in flight Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
87f0d5af48 |
fix: GH issues batch: deadlines opt-ins, SKV reconnect, narrative edit, payment-link gating (#1076)
* fix(errors): close remaining raw-message leaks after #1048 (#337) Follow-up to PR #1048. No user-visible toast or response field can now carry a raw engine or DB message; everything maps through getErrorMessage or the structured-errors registry. - get-error-message: only normalize a code-carrying Error instance into the structured path when the registry knows the code; unknown codes (Node system errors, stray third-party codes, Error-wrapped Postgres SQLSTATEs) fall through to pattern match, Swedish check, Postgres map and the status/context/generic fallbacks instead of returning the raw message. New Swedish-detection pattern for "ar last" phrases and a known-pattern row for "already has a journal entry". - structured-errors: add CANNOT_EDIT_NON_DRAFT (409) and MANDATORY_DIMENSION_MISSING (400) rows, plus common Node network codes (ECONNREFUSED, ECONNRESET, ETIMEDOUT, ENOTFOUND, EAI_AGAIN, EPIPE) as retryable 503 transients with a Swedish message. - pending-operations commit + bulk-commit routes: map executor error strings through getErrorMessage before responding (raw stays in logs); Swedish passes through, English falls to status-appropriate Swedish. - pending page: toast via getErrorMessage, fixing raw English toasts and "[object Object]" for structured envelopes on commit/bulk/reject. - transactions book + journal-entries routes: untyped catch and DB list errors no longer return err.message; mapped or static Swedish instead. - invoice send + issue-credit-note: partial_failures reasons are now Swedish (raw provider/DB text logged, never returned). - Tests: new unknown-code/Error-instance suite, registry rows asserted, route tests updated off the pinned raw-English expectations. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(skatteverket): target the räkenskapsår for yearly VAT redovisningsperiod A yearly filer with a broken fiscal year has a Skatteverket period ending in its FY-end month, not December, and the panel's year state is never maintained in yearly mode (the year picker is replaced by the räkenskapsår selector), so calls targeted the wrong period even for calendar-FY companies filing after year end. The selected fiscal period now rides through the whole chain: panel query strings, draft/validate/ submit bodies, buildMomsuppgift (which resolves the FY bounds so the period id and the figures describe the same räkenskapsår), and the staged-commit path. MCP callers without a fiscal period keep the calendar fallback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(deadlines): group same-day skattekonto deadlines into one card Moms, AGI and preliminärskatt legally share the skattekonto date (den 12:e), so a small monthly-moms employer saw 2-3 near-identical rows per month. Two or more pending system rows of the skattekonto family on the same due date now render as one grouped card with the date block once and each obligation as a sub-row keeping its own confirm-to-complete flow. Presentation only: rows, statuses, ICS feed unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(deadlines): KU + ROT/RUT + long-tail opt-in deadlines, rolling horizon Follow-ups from the #1028 audit left out of the #1057-#1060 fix stack, each with its own condition modeling: - kontrolluppgifter (KU10/KU20/KU31), due 31 Jan (SFL 24 kap. 1 §): opt-in flag suggested from ledger signals (2898 utdelning, 2393/2893 ägarlån; deliberately not 2091, see DECISIONS.md), AB only, mirroring the #1059 EU-sales suggest-and-confirm pattern. - rot_rut_begaran, due 31 Jan after the payment year (Lag 2009:194 8 §): rows generated only for years with actually PAID ROT/RUT invoices, resolved inside the generator; invoice-derived suggestion. - Long tail, explicit opt-in ('Fler deadlines'): OSS quarterly and IOSS monthly with a skipBankingDayAdjustment config flag (EU-law dates stand on weekends), Intrastat (10th banking day of the following month), punktskatt (ordinary skattedeklaration schedule), and fyllnadsinbetalning (12th of 2nd month over 30k / 3rd of 5th month, SFL 62:8 + 65 kap.). Kvarskatt deferred: needs a slutskattebesked date the app does not hold. - Rolling generation horizon: recurring types ~6 months ahead, annual 12 months, mirrored in the backfill expectation keys so the nightly cron never thrashes; regeneration now preserves manual in_progress status; one-time cleanup migration removes existing far-future rows. Migrations also applied to the staging branch, together with the previously missing 20260717xxxxxx deadline migrations (staging had drifted and lacked dismissed_at). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(arsredovisning): keep narrative editable after year-end close The narrative save endpoint refused writes whenever the fiscal period was closed/locked, but Verkstall bokslut closes the period before the arsredovisning text is ever written, so every legitimate save failed with PERIOD_LOCKED and the PDF fell back to placeholder text. The narrative is arsredovisning document text (ARL 6 kap.), not journal rakenskapsinformation, so the bookkeeping period lock does not apply. Saves are now refused only once a Bolagsverket submission for the period is registrerad (ARSREDOVISNING_REGISTERED, 409); the filed artifact was already frozen separately by the submissions immutability trigger. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(skatteverket): surface dead SKV connections and nudge reconnect Prod has ~70 companies that connected Skatteverket before the post-connect sync fix (#1010) and silently never synced skattekonto: the only reconnect prompt lived in the settings panel nobody revisits. - transactions-page banner when the connection is needs_reconsent or expired without refresh, linking to /settings/tax - pre-connect note in the connect panel: approve ALL behorigheter on Skatteverket's consent page (previously only shown after a failure) - wire the inert skattekonto.connection.expired event to an email nudge to the token owner; one send per consent episode via claim-first dedup in notification_log (type skv_connection_expired, partial unique index in migration 20260720090000, applied to staging) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(archive): per-year behandlingshistorik covers late-booked vouchers + Drive backup disclaimer The per-fiscal-year archive filtered audit rows by created_at within the period, dropping treatment history for bokslut entries, stornos and SIE imports booked after year end (BFNAR 2013:2 kap 8). The year archive now unions the date window with every audit row touching the period's journal entries and lines, deduped by audit id; line rows (company_id NULL by trigger design) are admitted via a scoped OR and reachable on the service-role backup path. ARCHIVE_FORMAT_VERSION 2->3 forces a one-time Drive re-upload so existing archives pick up the complete history. The Drive card on /import Exportera and the LASMIG texts now state the Drive copy is a convenience backup, not the BFL 7 kap legal archive. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(decisions): clarify Arsredovisning narrative save behavior on submission status * feat(invoices): gate payment links behind invoice settings opt-in The payment-link section (manual URL field + Stripe auto-create toggle) was visible on every invoice and auto-created Stripe links on send for any connected company. It is now opt-in per company: - new company_settings.invoice_payment_links_enabled, default false for everyone (no grandfathering of Stripe-connected companies) - invoice editor hides the whole section unless enabled; a draft that already carries a link still shows it so old links stay clearable - enforced server-side in maybeCreatePaymentLinkForInvoice (after the provider lookup, so the extension-free core build never queries), so dashboard, v1, MCP and recurring sends all obey it - new toggle on Settings -> Invoicing, saves instantly; sv/en strings Migration applied to the staging branch; prod gets it on merge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(tests): add invoice_payment_links_enabled to company settings fixture The makeCompanySettings fixture missed the new required boolean, failing the core-only build's type check of tests/helpers.ts. Default false, matching the migration default. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Emil <emilmattsson14@gmail.com> * fix(review): address CodeRabbit, compliance and Swedish review findings Round 2 of PR #1076 review feedback, one change per accepted finding: - pending page: res.json() safe fallback in both commit paths so a non-JSON proxy response cannot surface a raw parser error - bulk-commit: map operation status enums to Swedish display labels in the 'Redan hanterad' skip message - payment-link settings: disable the toggle while a save is in flight to prevent out-of-order PUT responses - deadlines group card: route all UI strings through next-intl (deadlines namespace, sv + en) - archive export: scope the period audit entry lookup to posted/reversed, matching the rest of the export - error tests: assert the exact registry English message for ECONNREFUSED to lock the no-leakage contract - signal routes: log.warn when best-effort lookups swallow a Supabase error (forensics), keep fail-closed behavior - narrative route: document that 'avslutad' submissions deliberately stay editable (never registered at Bolagsverket) - VAT: yearly declarations without an explicit fiscalPeriodId now resolve the räkenskapsår ending in the target year from fiscal_periods instead of assuming a calendar FY (SFL 26 kap 10-11 §§); calendar fallback only when no fiscal period exists - deadlines: IOSS deadline no longer requires vat_registered (Art. 369s has no Swedish VAT registration prerequisite) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Emil <emilmattsson14@gmail.com> --------- Signed-off-by: Emil <emilmattsson14@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
9c8e540338 |
fix(invoices): repair send dialog fiscal-period query + editable issu… (#1066)
* fix(invoices): repair send dialog fiscal-period query + editable issuance lines The send/mark-sent dialog queried fiscal_periods with start_date/end_date instead of period_start/period_end; the query always 400ed, and since PR #1023 made that fatal the dialog closed instantly, blocking mark-as-sent and email send for everyone. Also lets accrual companies edit the proposed journal lines before booking (both send and mark-sent), mirroring the mark-paid editor: untouched proposals still book via the server generator; edited lines book verbatim with balance validated at three layers. Credit notes and periodiserade invoices keep the read-only preview. The dialog now also respects defer_invoice_booking (#967). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(invoices): harden custom issuance-line validation per review findings Extract the custom-line parse + balance check into a shared validator so the send and mark-sent routes cannot drift. Reject rows carrying both debit and credit, and 29xx interim accounts (custom lines skip accrual schedule creation, so a 29xx balance would never be dissolved). Validate the payload only after the invoice ownership fetch, and emit structured log events when user-edited lines are booked or deliberately ignored, so manual overrides are visible in audit review. Account existence needs no route-level check: the engine already resolves every account against the company chart and throws AccountsNotInChartError. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(invoices): address CodeRabbit findings on issuance line editing Reject malformed JSON bodies with 400 instead of silently booking generated lines; restrict line editing to SEK invoices (custom lines cannot carry FX metadata); round each line before the client balance check to match the server; stop claiming a voucher was created in the mark-sent toast for deferred-booking companies; add programmatic labels to the editor inputs and remove-row buttons. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
05b954ac1d |
feat(deadlines): årsstämma replaces bokslut + moms_yearly auto-complete + EU-sales suggestion (#1059)
* feat(deadlines): gate F-skatt reminders on debited preliminary tax, add durable dismissal The f_skatt deadline was gated on the F-skatt approval flag (DB default true), giving nearly every company 12 monthly payment reminders for a tax Skatteverket may not have debited at all (64% of all system deadline rows, one lifetime completion). Approval carries no recurring obligation; the monthly duty is payment of debiterad preliminarskatt and exists only while the debited amount is > 0 (SFL 62 kap. 4-5 par., 55 kap. 2 par.). - Gate the f_skatt deadline on preliminary_tax_monthly > 0 (field already collected at onboarding, previously unread) and retitle it as a payment. - Storforetag keep the 12th in August (January-only 17th, 62 kap. 3 par.). - Declare the prod-only preliminary_tax_monthly column in a migration so installs built purely from migrations stop failing tax-settings saves. - Add deadlines.dismissed_at: DELETE on a system deadline now soft-dismisses it durably (hard deletes were resurrected by the nightly backfill within 24h); generator, backfill, and every read surface respect it. - Prune upcoming f_skatt rows for companies with no debited amount. Closes part of #1028. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(deadlines): gate AGI on employer registration, stop completing AGI deadline at XML generation The arbetsgivardeklaration deadline was gated on pays_salaries, which is wrong in both directions: a registered employer must file AGI every month including nil months (SFL 26 kap. 3 par.), and companies actively running payroll with the flag off got no AGI reminders at all (each missed monthly filing risks a forseningsavgift). - New company_settings.employer_registered (nullable, no default) gates AGI and the storforetag skatteinbetalning row; pays_salaries remains a fallback for rows saved before the flag existed and keeps its UI meaning. - Migration backfills employer_registered=true from pays_salaries=true and from actual payroll activity (salary_runs). - New employer_seasonal flag: sasongsregistrerade file only for payment months plus a December nil declaration, so only the December-period row is generated. - Settings UI: registration + seasonal checkboxes (sv/en strings). - AGI XML generation no longer auto-completes the deadline as submitted: SFL 26 kap. deems the obligation satisfied only when the declaration has come in to Skatteverket. The Skatteverket extension's kvittens reconcile remains the confirming path; manual filers tick the deadline themselves. Part of #1028. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(deadlines): statutory arsstamma replaces bokslut, moms_yearly auto-complete, EU-sales suggestion - Replace the non-statutory 'bokslut' deadline (3 months after FY end, no legal basis, off-by-one month math for broken FYs) with the statutory arsstamma deadline: within 6 months of FY end per ABL 7 kap. 10 par., the corporate act that gates the arsredovisning filing chain. Migration deletes pending bokslut rows; the backfill cron generates arsstamma rows. - Complete moms_yearly on Skatteverket submission/kvittens: the yearly branch previously returned null with a stale comment claiming annual VAT has no deadline type, leaving yearly filers with an eternally open row. The fiscal-year tax_period label is derived from company settings. - Add /api/settings/eu-trade-signal + a tax-settings callout: companies with booked EU sales (3108/3308/3107, last 15 months) but EU-trade/PS flags off are prompted to confirm the periodisk sammanstallning obligation (SFL 35 kap., 1 250 kr late fee per report). Suggestion only, never auto-enables. Part of #1028. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(deadlines): include dismissed_at in DeadlineForm payload The Deadline type gained the required dismissed_at field; the form's submit payload literal must carry it for the Omit<Deadline, ...> shape. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(deadlines): make system-deadline dismissal atomic Constrain the dismiss update to source='system' and verify a row was actually updated: a concurrent regeneration can delete the row between lookup and update, and the route must not report a phantom success. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
da4d5a39ae |
feat(deadlines): gate AGI on employer registration + stop completing AGI at XML generation (#1062)
* feat(deadlines): gate F-skatt reminders on debited preliminary tax, add durable dismissal The f_skatt deadline was gated on the F-skatt approval flag (DB default true), giving nearly every company 12 monthly payment reminders for a tax Skatteverket may not have debited at all (64% of all system deadline rows, one lifetime completion). Approval carries no recurring obligation; the monthly duty is payment of debiterad preliminarskatt and exists only while the debited amount is > 0 (SFL 62 kap. 4-5 par., 55 kap. 2 par.). - Gate the f_skatt deadline on preliminary_tax_monthly > 0 (field already collected at onboarding, previously unread) and retitle it as a payment. - Storforetag keep the 12th in August (January-only 17th, 62 kap. 3 par.). - Declare the prod-only preliminary_tax_monthly column in a migration so installs built purely from migrations stop failing tax-settings saves. - Add deadlines.dismissed_at: DELETE on a system deadline now soft-dismisses it durably (hard deletes were resurrected by the nightly backfill within 24h); generator, backfill, and every read surface respect it. - Prune upcoming f_skatt rows for companies with no debited amount. Closes part of #1028. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(deadlines): gate AGI on employer registration, stop completing AGI deadline at XML generation The arbetsgivardeklaration deadline was gated on pays_salaries, which is wrong in both directions: a registered employer must file AGI every month including nil months (SFL 26 kap. 3 par.), and companies actively running payroll with the flag off got no AGI reminders at all (each missed monthly filing risks a forseningsavgift). - New company_settings.employer_registered (nullable, no default) gates AGI and the storforetag skatteinbetalning row; pays_salaries remains a fallback for rows saved before the flag existed and keeps its UI meaning. - Migration backfills employer_registered=true from pays_salaries=true and from actual payroll activity (salary_runs). - New employer_seasonal flag: sasongsregistrerade file only for payment months plus a December nil declaration, so only the December-period row is generated. - Settings UI: registration + seasonal checkboxes (sv/en strings). - AGI XML generation no longer auto-completes the deadline as submitted: SFL 26 kap. deems the obligation satisfied only when the declaration has come in to Skatteverket. The Skatteverket extension's kvittens reconcile remains the confirming path; manual filers tick the deadline themselves. Part of #1028. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(deadlines): include dismissed_at in DeadlineForm payload The Deadline type gained the required dismissed_at field; the form's submit payload literal must carry it for the Omit<Deadline, ...> shape. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(deadlines): make system-deadline dismissal atomic Constrain the dismiss update to source='system' and verify a row was actually updated: a concurrent regeneration can delete the row between lookup and update, and the route must not report a phantom success. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
5b8e3fa130 |
fix(vat): enforce decimal vat_rate on supplier invoice items and normalize MCP percent extraction (#1049)
Supplier invoice items store vat_rate as a decimal fraction (0.25) while customer invoices use integer percent (25). The shared Zod schema accepted 0-100, so a percent-shaped vat_rate silently booked 2500 % VAT via line_total * vat_rate, and the MCP inbox-conversion path staged the AI extraction's percent-integer vatRate straight into the decimal column with per-line vat_amount 0. Part of #310. - CreateSupplierInvoiceItemSchema.vat_rate is now a literal union of the statutory decimal set (0, 0.06, 0.12, 0.25) with a unit-hint error, covering the cookie route, the invoice-inbox convert route, and /api/v1 (whose runtime ALLOWED_SV_VAT_RATES guard stays as defense in depth). - New shared normalizeVatRateToDecimal() in lib/vat: percent-shaped values (25, 12, 6) divide by 100, results snap to the legal Swedish set, and anything else (foreign 19/20, non-finite) maps to 0. - gnubok_create_supplier_invoice_from_inbox normalizes vatRate at the extraction boundary and derives per-line vat_amount when the extraction carries none, so the staged header vat_amount is honest. - The pending-operation executor normalizes staged vat_rate on insert, so rows staged before this fix cannot book percent-scaled VAT. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a5e37d3510 |
Fix/build (#1041)
* fix(bookkeeping): harden correction account changes * feat(tax): enhance tax deadline generation with new settings and filing methods - Added new company settings: tax_turnover_over_40m, vat_has_eu_trade, vat_filing_method, periodisk_sammanstallning_enabled, and periodisk_sammanstallning_filing_method. - Updated deadline generation logic to accommodate new settings affecting VAT and employer declaration deadlines. - Implemented tests for new functionality, ensuring that completed obligations are preserved and not replaced by new pending rows. - Introduced a cron job to backfill missing tax deadlines for companies with settings but no upcoming deadlines. - Updated API routes for generating tax deadlines and handling cron jobs. - Modified database schema to include new columns for tax filing profiles and constraints for filing methods. * fix(invoices): record credit note reconciliation guard * fix(tax): correct automatic deadline settings * fix(tax): key AGI deadline to VAT taxable base and add storforetag payment deadline The 26th filing day for the skattedeklaration (AGI and VAT together) hinges on one statutory measure, a VAT taxable base above SEK 40 million (SFL 26 kap.), not a separate employer turnover. Drop employer_turnover_over_40m and derive the AGI schedule from vat_registered plus vat_taxable_base_over_40m, so a non-VAT-reporting employer is never shown the 26th when its binding date is the 12th. Also: - add a skatteinbetalning deadline row (12th, 17 January) for storforetag, whose deducted tax and employer contributions are due before the 26th filing date - normalize legally incoherent over-40m flag combinations to the earlier small-company schedule in a follow-up migration - replace hardcoded 27 December dates with the banking-day adjustment - extend the 40m help text to cover the SKV-decided early filing election and the payment-still-on-the-12th rule - document the regeneration race repaired by the daily backfill cron Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(migrations): add AGI and VAT filing logic with employer column removal * feat(settings): implement VAT registration logic and update related flags; enhance deadline handling --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
1443235cec |
feat(invoices): registrera utan att bokföra + explicit Bokför-steg (#1040)
* feat(invoices): registrera utan att bokföra + explicit Bokför-steg Companies where one person registers supplier invoices / sends customer invoices while ekonomi does the actual bookkeeping had no way to split the two: under faktureringsmetoden every registration/send booked the journal entry inline. - New company setting defer_invoice_booking (default off, accrual only): registering a supplier invoice or sending/marking-sent a customer invoice creates NO journal entry. - New explicit booking routes POST /api/supplier-invoices/[id]/book and POST /api/invoices/[id]/book: create the registration/revenue entry afterwards, CAS-guarded against concurrent booking (a lost race cancels the just-posted voucher with a gap explanation), including periodisering schedules. - Detail pages show "Ej bokförd ännu" + a Bokför button for unbooked accrual invoices; the settings toggle lives under Bokföringsmetod. - mark-paid needs no changes: both payment flows already route on the journal-entry link, so an invoice still unbooked when paid gets the full cash-style entry. - The mark-sent fail-closed rollback now keys on the same gate so deferred sends are not rolled back as booking failures. Fixes #967 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(invoices): harden deferred booking after review CodeRabbit round on #1040: - CAS link guards also require a still-bookable status (and uncredited, customer side) so a concurrent mark-paid/credit cannot end up with a double-posting registration/revenue entry. - Settings reads fail closed instead of defaulting to accrual rules. - Detail pages surface the ACCRUAL_SCHEDULE_FAILED warning instead of showing plain success, and the customer page no longer stringifies structured errors into "[object Object]". - The settings form normalizes defer_invoice_booking to false under kontantmetoden so a stale flag cannot re-activate on method switch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6bd85f94b6 |
fix(bookkeeping): editable verifikationstext on andringsverifikation (#1035)
The correction header was always built server-side as "Rattelse: <original description>". When the original entry was labelled after the wrong account, the correction kept echoing that stale label even after the user switched to the correct account (follow-up to the line-description fix in #1029). - CorrectJournalEntrySchema gains an optional trimmed description - correctEntry() accepts options.description; blank or absent falls back to the canonical "Rattelse: <original>" auto text - both correct routes (dashboard + v1, which share the schema) thread the description through - CorrectionEntryDialog surfaces an editable verifikationstext field, pre-filled with the auto text; an untouched or cleared prefill is NOT sent, so the server-side fallback stays the source of truth (same only-overwrite-auto-filled principle as #1029) Forward-only: already-posted corrections are immutable per BFL. Fixes #1031 Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
edef48471c |
feat: add currency for articles (#834)
Co-authored-by: Jakob Wennberg <jakob.wennberg@gmail.com> |
||
|
|
072aedeaf9 |
Fix/supp ag fb (#1023)
* fix: prevent credit notes from entering payment flow * fix: persist and display customer personal numbers * feat: configure automatic invoice reminder days * fix: issue credit notes through send flow * chore: add repository agent guidance * feat(mcp): route tools across user companies * fix(articles): delete unused register entries * feat(invoices): improve issued invoice actions * feat(supplier-invoices): retain uploaded source documents * docs: record implementation decisions * feat: enhance customer personal number handling and validation - Updated CustomerForm to allow personal numbers in the format of "********-1234" for individual customers. - Added validation to ensure personal numbers are only accepted for individual customers in CreateCustomerSchema. - Implemented masking and encryption for personal numbers to enhance data protection. - Introduced new utility functions for masking and encrypting personal numbers. - Added database migration to enforce unique constraints on credit note relationships and prevent duplicate entries. - Enhanced error handling and logging for credit note issuance and invoice processing. - Updated tests to cover new credit note creation guards and personal number handling. * test: enhance list companies test with supabase query mocks |
||
|
|
b6332e9ff4 |
Fix/skv connection flow (#1015)
* feat(salary): one-click AGI submission with filing state machine and success feedback The AGI panel required users to know that "Ladda ner AGI-fil" was the generate step, then click submit, signing link, and kvittens manually. A nollkorning filing stalled on "AGI-XML saknas" pointing at a UI path that does not exist. - New primary button "Lamna in till Skatteverket" chains the existing endpoints client-side: generate XML if missing, POST underlag, poll kontrollresultat, create signing link, open Mina Sidor in a tab opened synchronously at click (popup-blocker safe). Inline stepper shows each step; the four old buttons become collapsed advanced/recovery actions, auto-expanded in stale-draft and rejected states. XML download stays visible and free for manual filing. - deriveAgiFilingState() + useAgiSubmission() lift the per-period submission record to the run page: the progress rail and salary hero now render the real state machine (generated, underlag inskickat, vantar pa BankID-signatur, inlamnad med kvittensnummer) instead of telling users to "lamna in" an already-submitted declaration. - Success card with kvittensnummer and signature metadata once signed, plus a toast when a poll flips the state while the page is open. - AGI kvittens cron every 15 min instead of every 2 h so filings signed on another device get stamped and emailed promptly. - Advanced submit also auto-generates, and the stale "Lon -> AGI -> Generera" error text now points at the real buttons. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(enable-banking): instant OAuth callback feedback and dead-attempt cleanup The bank redirect landed on a blank page for the several seconds the callback spent exchanging the PSD2 session and mirroring accounts, and every failed connect attempt left a status='error' row that rendered forever as an "Atgard kravs" card next to a successful retry, showing duplicate connections to the same bank. - Stream a branded "Slutfor bankanslutningen" progress page from the callback: the shell flushes before the session exchange starts and a script/meta redirect follows when the work completes, with a 30s slow-work escape hatch. Fast outcomes (denial, bad params, unknown state) keep their plain redirects. - Delete never-activated connection rows (no session_id, no accounts_data) on denial or exchange failure, and sweep leftovers for the same bank on the next connect. Established connections keep their "Atgard krävs" card via the accounts_data guard; FKs are ON DELETE SET NULL so deletion has no dependents. - Show "Banken ar ansluten: hamtar dina konton" while the settings panel loads after the callback instead of an anonymous spinner. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(invoices): reject re-send of issued invoices and gate bookkeeping on the sent flip A direct POST to /api/invoices/[id]/send against an already-issued invoice re-emailed the customer and posted a second revenue verifikat (createInvoiceJournalEntry has no dedup), overwriting journal_entry_id and orphaning the first entry. Only the UI hid the button; the v1 route and the MCP commit executor already rejected non-drafts. - Non-draft invoices now return 409 INVOICE_ALREADY_SENT. - The draft to sent status flip is an optimistic lock (status guard plus row-count check); journal entry, accrual schedules, PDF archival and the invoice.sent event only run for the request that won the flip. - On a flip failure the journal entry is deferred: the row stays draft and a retry re-runs the pipeline, ending with exactly one verifikat. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(invoices): payment links, failure visibility and sandbox guard for recurring auto-send - sendInvoiceFromSchedule now auto-creates an online payment link via applyPaymentLinkToInvoice before rendering and passes the payment link QR to the PDF: parity with the dashboard and v1 send routes, which recurring invoices silently lacked. - The recurring cron persists last_run_warning both when a claimed run throws (hourly retries stay visible on the schedule) and when a stale schedule is rolled forward, so a deterministic failure can no longer skip a month silently. - Auto-send is blocked for sandbox companies at the email chokepoint (freeze-and-retain: the invoice is still generated as a draft), covering both the cron and the run-now route with one guard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(salary): close the Fortnox payroll API gaps (phases 1-4) Payroll now runs end-to-end through the open API, including onboarding a client from another payroll system, with every write staged for approval. - v1: per-employee payslips (list/detail/PDF), payslip line writes, run roster attach/remove, absence ranges (per-day storage), jamkning fields, cutover opening balances (single + atomic bulk PUT), vacation balance + vacation-year-close. PUT added to the wrapper's idempotency/ test-key set (test keys could otherwise write through PUT). - MCP: 10 new tools (get_employee/get_payslip/list_absence/ get_vacation_balance reads + staged update_payslip_line, register_absence, create_employee, update_employee, set_employee_opening_balances, close_vacation_year), executors, risk tiers, op-type CHECK expansions. create_employee encrypts personnummer at staging: pending_operations never holds plaintext. - Scope-map audit retrofit: 11 formerly unmapped tools now scoped; BREAKING for keys that relied on the 4 default-allow writes. - Cutover: employee_opening_balances (derived lock trigger, self-unlocks on run correction), engine YTD/karens/liability integration, Ingaende saldon section in the employee editor. - Arbetsschema-lite: employees.hours_per_week/workdays_per_week drive the hourly/daily divisors; legacy 173/21 preserved exactly at defaults so existing pay math is byte-identical. - Vacation ledger + semesterberedning/arsavslut: recomputed per-year day balances (synced on book/correct, non-fatal), year-close with the min-20 floor, 5-year sparade-dagar expiry to forced payout, and a 2920/2940 drift adjustment via the bookkeeping engine; Semester dashboard card with preview-then-confirm dialog. - Fix: Zod 4 defaults leak through .partial(), which made every sparse employee PATCH fail validation and reset defaulted columns. Migrations 20260713100000/101000/110000/121000/122000 (applied to staging with version rows; prod via merge). vacation_ledger renamed from 20260713120000 to avoid colliding with vat_declaration_totals_rpc. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * perf: cut dashboard page-load latency (region, round trips, caching, VAT RPC) The dominant cost was infrastructure: Vercel functions ran in iad1 (Washington D.C.) while Supabase (DB + auth) lives in eu-north-1 (Stockholm), so every request paid 4-5 transatlantic round trips of auth + company resolution before doing any real work (measured 530-1900ms for single-query GETs in prod logs). Pin functions to arn1 and cut the redundant work on top: - vercel.json: functions to arn1, same city as the database - getActiveCompanyId: preference + first-membership queries run in parallel; the fallback result doubles as validation in the common single-company case (one round trip instead of two sequential) - withRouteContext: Server-Timing header and authMs/companyMs/handlerMs in the op-completed log, so latency is attributable per phase - dashboard layout: nav badge counts off the critical path; DashboardNav loads them client-side via the new use-worklist-badges SWR hook with debounced realtime revalidation - swr (new dependency, approved): global provider; useCompanySettings shares one cache entry across consumers and renders from cache on back-navigation instead of re-showing skeletons - /pending: realtime refetch debounced; bulk operations previously fired 4 requests per row-change event - VAT declaration: new get_vat_declaration_totals RPC returns per-account totals, settlement-shape detection (#984) and source_type counts in ONE round trip instead of paging every entry+line through PostgREST. Account lists stay TS-side parameters so ACCOUNT_RUTA remains the single source of truth. Shape-exclusion coverage moved to tests/pg/vat-declaration-totals-rpc.pg.test.ts; DDL already applied to staging. - bundle: CommandPalette lazy-mounts on first Ctrl/Cmd+K, AgentChat dynamic-imports the markdown parser, @vercel/speed-insights (new dependency, approved) added for real-user timings The /salary fetch-waterfall fix from the same effort already landed inside 2084a756. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(invoices): settle öre-rounded payments from the mark-paid flow An invoice with öresavrundning shows a rounded "Att betala" on the PDF; the customer pays that amount (up to 50 öre off the stored öre total) and the invoice-page mark-paid flow rejected it with MATCH_AMOUNT_EXCEEDS_REMAINING: a dead end, while the bank-transaction match flow already absorbed the residual to 3740. - PaymentBookingDialog now proposes the rounded bank leg plus the 3740 residual line (credit when rounded up, debit when rounded down), resolved via getDisplayTotal from the per-invoice override and company_settings.ore_rounding. - settleInvoicePayment and the v1 mark-paid route absorb the sub-krona residual, gated by planInvoicePaymentForLines: absorption applies ONLY when the caller lines carry the exact residual on 3740; otherwise the strict plan applies (sub-krona partials stay partial, no-3740 overshoots keep the 400), so the GL can never diverge from the AR sub-ledger. - planInvoicePayment absorb-band boundary tightened to >= 1 kr: an exactly-1-kr overshoot used to slip past both the guard and the absorb branch and silently over-record paid_amount (pre-existing on the bank-match path). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(security): resolve all 7 PR compliance findings - ASVS V3.3: per-request CSP nonce on the enable-banking finalize page (mirrors the mcp-oauth consent page); inline scripts are nonce-bound - ASVS V16: decouple callback finalize work from the response stream (eager promise + next/server after()) so a client disconnect cannot drop session persistence or the consent_granted audit emit - ISO 27001 A.8.15: failed audit-event emits log through the structured logger with a stable message for log-based alerting - ASVS V2.3: recurring-invoice cron and run-now routes resolve isSandboxCompany themselves and pass an explicit suppressAutoSend flag (defence in depth around the email chokepoint, freeze-and-retain kept) - ISO 27001 A.8.11: stagePendingOperation rejects plaintext personnummer-bearing keys in params/preview_data (key-based guard; EF org numbers make value-matching unsafe) - ASVS V4.5: employee PATCH body is truly sparse; cleared number fields are omitted instead of resetting DB values to hardcoded fallbacks - ASVS V8.2.1: route-level tests pin the v1 cross-company deny (404 by convention, not 403) on the payslip PDF endpoint Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: implement vacation-year basis change validation and error handling - Added tests to block vacation-year basis changes when open balances exist. - Implemented error handling for open-balances guard query failures in the settings route. - Enhanced absence route to reject reversed date ranges with a validation error. - Updated absence handling to use atomic upserts instead of delete+insert for better performance and reliability. - Refactored salary calculation logic to correctly handle age-based avgifter rates according to Skatteverket's rules. - Improved error messaging for vacation year closure adjustments. - Adjusted employee opening balances handling to preserve audit information during upserts. * feat(settings): add validation to block vacation-year basis change with open balances feat(absence): reject reversed date ranges in absence queries fix(absence): update absence handling to use atomic upserts instead of delete+insert fix(employee): improve validation for jamkning dates in employee updates fix(opening-balances): ensure created_by field is preserved during upserts test(absence): enhance tests for absence range and date validations test(calculation): add tests for age-based avgifter rates and edge cases test(semesterberedning): validate vacation year closure adjustments and error handling test(employee-opening-balances): update tests to reflect changes in salary_run_employees schema * fix(migrations): implement NOT VALID constraints for pending_operations and add validation migration --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
98d0c7f2d0 |
Add/stripe skv (#1004)
* fix(salary): align pain.001 salary file with the Swedish domestic bank dialect Verified against the Swedish Common Interpretation of ISO 20022 (Bankforeningen, Common Payment Types in Sweden, Appendix 1 Example 4: Salaries) and Nordea Corporate Access pain.001 examples v2.6 (2026-06-22), and XSD-validated against the official pain.001.001.03 schema: - drop SvcLvl SEPA (SEPA credit transfers are EUR-only; omitting SvcLvl gets the domestic NURG default) - drop RmtInf (not allowed for SALA salary payments; the beneficiary statement text comes from the Dataclearing LON code) - address employees domestically: clearing as CdtrAgt ClrSysMmbId SESBA, account WITHOUT clearing as CdtrAcct Othr with SchmeNm BBAN - share the clearing/account split (Swedbank 5-digit shift, Nordea personkonto prefix dedup) between the LB and pain.001 generators via splitDomesticBankAccount, fixing pain.001 duplicating the personkonto clearing - clamp MsgId/PmtInfId/InstrId/EndToEndId to Max35Text with the per-tx counter surviving truncation; carry the org number on Dbtr - return 400 from the pain001 route on an invalid clearing instead of emitting a broken file Also includes two unrelated decision-log lines from the parallel revisor-review session (DECISIONS.md is a shared append-only log). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(nav): surface the year-end chain in the sidebar Add Periodiseringar, Arsredovisning (aktiebolag only) and Inkomstdeklaration (INK2 for AB, NE-bilaga for EF) to the Skatt & bokslut group, in workflow order. Entity gating via a new entityOnly flag on NavItem; isActive carve-outs extended so exactly one row lights up for the new routes. Driven by an external revisor review that concluded these features did not exist because none of them were reachable from the nav. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(stripe): Stripe Connect integration behind config gate Connect OAuth per company (only the acct_ id is stored), automatic single-use Payment Links on invoice send, deterministic payment settlement against 1686 (BAS moved acquirer receivables 1580 -> 1686), payout booking with reverse-charge fees (6570 + 4535/4598 + 2645/2614), and a 15-minute sync cron. Non-deterministic events land as needs_review, never guessed at. Fully dark without STRIPE_CONNECT_CLIENT_ID: connect returns 503, the send hook and cron no-op, and the settings page shows 'Kommer snart' (hosted) until the Connect platform is verified. Self-hosted keeps the honest not-configured message. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(deadlines): add shared completeTaxDeadline and fix dead AGI deadline auto-complete generate-declaration.ts has updated non-existent columns (type/period/ status) since inception, so the arbetsgivardeklaration deadline was never auto-completed. Replace with a shared helper targeting the real schema (tax_deadline_type/tax_period/is_completed), also used by the kvittens crons and moms handlers in the follow-up commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(rot-rut): import Skatteverket beslutsfil and record decisions on payout requests Parse the beslutsfil JSON from Skatteverkets rot/rut e-tjanst and record godkant belopp on the matching begaran: matched by stored skv_referensnummer first, then exact name among active undecided requests; arenden by fakturanummer then personnummer, exactly-one or the beslut errors (all-or-nothing). Never auto-settles: recording the beslut and booking the payout are separate acts. Exposed as an API route and the gnubok_import_rot_rut_beslut MCP tool. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(skatteverket): system auth for background reads, one-click VAT submit, kvittens notifications Hybrid auth program: system CCG (org certificate) for background reads while personal BankID stays for interactive submissions, since SKV per-flow refresh tokens live 65 min and crons structurally cannot run on them. All system-auth code sits behind SKATTEVERKET_SYSTEM_AUTH_MODE (default off) with a stub transport until the Expisoft cert and CCG avtal land; auth resolution is centralized in resolve-auth.ts. Also in this change: - One-click VAT submit chaining kontrollera -> utkast -> las server-side with a stage discriminator; step-by-step buttons demoted to the overflow menu. - Kvittens crons (AGI + new VAT schedule) with email-only notifications, deduped in notification_log under the new skv_kvittens type. - Ombud grant probe + verification UI in the connect panel, and a dashboard promo card for unconnected companies. - skatteverket_company_connections table with pg-real coverage. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(salary): auto-settle AGI tax payment from skattekonto and surface SKV reconnect on the tax card The "Skatt att betala" card only cleared via the manual mark-paid button on the run detail page; the promised automatic flip from the Skattekonto sync was never implemented, so paid periods stayed red. - settleAgiTaxPayments: during every skattekonto sync, a booked "Arbetsgivardeklaration YYYYMM" debit row settles the matching agi_declarations.tax_paid_at, but only when the amount equals the declared total to the ore and the account is not in deficit (deterministic; drift or deficit falls back to manual). - Salary overview card: reconnect hint when the SKV token needs re-consent (link to /settings/tax, silent when the extension is off), plus an inline "Markera som betald" button reusing the existing endpoint and salary_payments strings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Add cloud backup scheduling and alerting features - Implement unit tests for scheduling logic in `schedule.test.ts`, covering various scenarios for determining if a backup schedule is due. - Create a new module `backup-alert.ts` to handle failure alerts for cloud backup auto-sync, including email notifications for reauthentication and repeated failures. - Introduce `schedule.ts` to manage scheduling logic, including handling local time zones and converting between local and UTC hours. - Add CSV report generation functions in `archive-csv.ts` for trial balance, income statement, balance sheet, and general ledger, ensuring compatibility with Swedish Excel formats. - Create a README generator for the archive structure in `archive-readme.ts`, providing clear documentation for users accessing backup files. - Implement tests for CSV report generation in `archive-csv.test.ts`, ensuring correct formatting and content. - Establish a full-archive coverage contract test in `full-archive-coverage.pg.test.ts` to ensure all company-scoped tables are properly classified for backup. * fix(stripe): correct invoice clearing reference and improve type safety in sync logic * fix(invoices): narrow accountingMethod before resolveInvoicePaymentSourceType settleInvoicePayment takes accountingMethod as a raw settings string, but resolveInvoicePaymentSourceType requires the 'accrual' | 'cash' union. Normalize at the call site (anything but 'cash' books as accrual), matching the existing useCashEntry semantics. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: address CodeRabbit review findings and nitpicks on PR #1004 Review findings: - backup settings redirect: always force view=export over incoming params - AGI/VAT kvittens crons: isolate best-effort post-submit calls, check the signed-state persist error, guard recovery calls in catch blocks so one company cannot abort the rest; surface grant_revoked in the run summary - kvittens notifications: atomic claim-first dedup with a partial unique index; map non-uuid reference keys to deterministic uuids - grant probe: record the actual 2xx status; mTLS transport: handle response-stream errors - stripe: amount-aware idempotency keys for payment links; emit stripe.disconnected on upstream revocations - ROT/RUT beslut import: mutate in-memory request state after apply, move item + header writes into an atomic apply_rot_rut_beslut RPC, add rot_rut_payout to JournalEntrySourceTypeSchema - migrations: use NOT VALID + VALIDATE CONSTRAINT for CHECK constraints on journal_entries, notification_log and rot_rut_payout_requests - cloud backup: hour_utc-only schedule updates clear stale hour_local Nitpicks: - stripe sync: enforce the cron time budget inside per-connection event processing with idempotent cursor progress; maybeSingle for settings; honest partial-customer DTO shared with the settlement boundary - shared applyPaymentLinkToInvoice helper for both invoice send routes, v1 docblock documents step 6b and PAYMENT_LINK_FAILED - settings panel: drop redundant decodeURIComponent - cloud backup: document worst-case archive memory headroom Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
7d7f604e00 |
Add/stripe invoice link (#998)
* feat(supplier-invoices): show registered invoices under "Att betala" with inline approve Registered supplier invoices are already booked as debt (2440) but were hidden from the "Att betala" tab until approved, which confused users. The tab now shows registered invoices too, marked "Ej godkand" with a compact inline approve button. Approval remains the gate for payment, not visibility; status model and approve API untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(reports): add date range filter to huvudbok (kontoanalys) Mounts the existing ReportDateRange control on /reports/huvudbok so the ledger can be narrowed to any date range within the fiscal year, matching Fortnox kontoanalys. Lines before the range roll into each account's opening balance so running balances stay correct at the range start; lines after the range are dropped. Applies to the XLSX export too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(invoices): add optional payment link on invoices (paste-link MVP) The user pastes a payment link created in their PSP dashboard (e.g. a Stripe Payment Link) onto an invoice. The recipient gets a "Betala online" button in the invoice email and a QR code + clickable link in the PDF payment box. No PSP integration server-side: this is the demand probe; a future Stripe Connect integration would auto-fill the same column. - invoices.payment_link_url (migration 20260709090000), https-only + 2048-char cap enforced in CreateInvoiceSchema; empty string normalises to undefined and build-invoice-write always writes a concrete value so clearing the field on a draft edit NULLs the column - editor field (real invoices only) with one-link-per-invoice hint; strings in sv+en (messages landed via e0e11066) - email button (customer.language, hidden for credit notes/proforma/ delivery notes, URL escaped for the href attribute) + URL in the plain-text part - PDF QR + link row following the Swish QR pattern; wired into send, download and preview routes - derived documents (credit note, proforma convert, recurring) do NOT copy the link: it encodes one amount for one specific invoice - MCP gnubok_create_invoice accepts payment_link_url (validated at staging and re-checked in the commit executor); v1 API exposes the column; tools/list token ceiling bumped 45K -> 45.5K (ledger entry in payload-size.bench.test.ts, headroom was <10 tokens) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(invoices): show oresavrundning on editor/form totals, supplier list and invoice email The rounding logic (getDisplayTotal) was correct but only applied on the PDF, invoice list/detail and review dialog. The invoice editor summary, the supplier invoice form totals and the supplier invoice list showed the raw ore total right next to the toggle, and the invoice email said "Att betala" with the unrounded invoice.total while the attached PDF showed the rounded amount (and the email also ignored the ROT/RUT deduction). Extract the PDF's Att betala block into getAmountToPay (lib/invoices/rounding.ts) and point PDF + email at it so they cannot drift; behavior-identical refactor for the PDF. Booked amounts stay ore-exact; display-only as designed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(reports): adapt huvudbok date-range tests to the two-step entry-lines fetch The date-range tests (0969168f) mocked the old single-query shape with the parent entry embedded on each line; main's refactor (fetchEntryLines) queries journal_entries first and reattaches. Queue entry rows like the other tests so the merge of the two features is actually exercised. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(invoices): fetch full invoice projection in v1 send so ROT/RUT deduction and payment link reach the PDF and email The v1 send route's hand-rolled column list omitted deduction_total, deduction_personnummer_last4, payment_link_url and the item-level ROT/RUT fields, so invoices sent via the public API overstated 'Att betala' and dropped the deduction box. Reuse the shared INVOICE_FULL_COLUMNS/INVOICE_ITEM_FULL_COLUMNS so the send row can never drift from the GET shape again. Also harden the supplier-invoice inline approve: a thrown fetch left the button stuck spinning; failures now refetch the true server state. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b4a21b1029 |
fix(documents): RFC 5987 Content-Disposition so NFD filenames stop crashing inline view (#964)
* fix(documents): RFC 5987 Content-Disposition so NFD filenames stop crashing inline view macOS/iOS uploads carry NFD-decomposed filenames (base letter + combining diaeresis U+0308, char code 776). undici Headers require ByteString values (every code unit <= 0xFF), so splicing the raw filename into the Content-Disposition header threw while building the response and the inline document route 500ed. 122 prod documents across 35 companies hit this; last crash 2026-07-09T16:17. Add lib/api/content-disposition.ts emitting the RFC 6266 dual form: an ASCII quoted fallback (NFC-normalize, then replace anything outside printable ASCII plus quote and backslash with _) and filename*=UTF-8''<percent-encoded> per RFC 5987 (encodeURIComponent on the NFC name, additionally escaping ! ' ( ) * which it leaves bare). Use it in the inline document route and in the two latent same-shape sites that embed raw employee names in payslip PDF headers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): sanitize lone surrogates before percent-encoding Content-Disposition (CodeRabbit) Unpaired UTF-16 surrogates survive normalize('NFC') and make encodeURIComponent throw a URIError, so replace them with U+FFFD via String.prototype.toWellFormed() before encoding so the helper always returns a valid header value. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
2e7931b36b |
feat(customers): let users set a customer number shown on the invoice (#957)
Implements #914 (kundnummer on customers, printed on the invoice PDF). - Migration: nullable text column customers.customer_number, no unique constraint in v1 so existing rows and imports keep working. - API: CreateCustomerSchema/UpdateCustomerSchema accept an optional customer_number (trimmed, max 32 chars, nullable-then-optional so the OpenAPI registry sees it as not required); create/update routes persist it and normalize empty string to null so it can be cleared. - v1 public API: customers create/detail/update round-trip the field (insert and update field lists, response projections, response schemas), and the invoices :send route fetches customer_number in its explicit customer join so the emailed PDF matches the downloaded one (the pdf route already selects customers(*)). - UI: optional Kundnummer field in CustomerForm (next-intl keys in both sv and en), wired into the edit dialog's initialData; read-only Kundnummer row on the customer detail page's business-details card. - Invoice PDF: renders "Kundnr:" / "Customer no.:" in the customer box when set; the PDF reads the live customers join, so no snapshot column is needed. - Tests: route tests cover 400 validation, trimming, clearing with null/empty, and omit-leaves-untouched on POST and PATCH; v1 tests cover the create/update round-trip (insert/update payload + response projection) and the :send customer-join projection. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c1ea0d9bf2 |
fix(salary): make pain.001 betalfil generatable (company IBAN + BIC) (#950)
The ISO 20022 pain.001 salary payment file could never be generated: the route required company_settings.iban/bic, but no settings screen wrote those columns, so every request returned 400. The specific reason was also swallowed by getErrorMessage (isSwedishUserMessage did not know "krävs"/"saknar"), surfacing only the generic "Förfrågan innehåller ogiltiga uppgifter" (issue #945). - Add IBAN + BIC inputs to Settings > Fakturering > Bankuppgifter. BIC auto-derives from the clearing number / bank already entered, so in practice only the IBAN is typed. Validated client- and server-side. - Route requires the company IBAN (canonical debtor form every Swedish bank accepts) and derives the BIC, with clear actionable errors. - Employees are unchanged: domestic clearing + account (BBAN), which is what Swedish payroll collects. Only the company (debtor) uses IBAN. - getErrorMessage recognizes "krävs"/"saknar" so payment-file reasons surface instead of the generic 400. Fixes #945 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
bacc5914af |
Fix/dependabot cus feedback (#946)
* feat(bookkeeping): per-account default VAT, oresavrundning momsfri Add a per-account "Standard moms" setting to the chart of accounts and use it to auto-fill the moms on a leverantorsfaktura-rad when that konto is picked. Oresavrundning (3740) ships as "Ingen moms", so a rounding line no longer inherits the 25 % rad-default and skews the moms. - chart_of_accounts.default_vat_rate (0/0.06/0.12/0.25, CHECK-constrained) - BEFORE INSERT trigger ships 3740 momsfri on every insert path; backfills existing 3740 rows - kontoplan editor: dead free-text momskod replaced with a Standard moms select - supplier-invoice rad auto-fills the rate from the konto default Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(supplier-invoices): configurable start number for the ankomstnummer series Add a company_settings.next_arrival_number start floor so a company can continue its leverantorsfaktura numbering from a previous system (e.g. Fortnox) instead of restarting the ankomstnummer at 1. get_next_arrival_number now floors the series via GREATEST(MAX(arrival_number)+1, next_arrival_number), so the floor can never move the series backwards or collide with the (company_id, arrival_number) unique index. The RPC is hardened while rewritten: SET search_path to empty, schema-qualified refs, and an auth.uid() membership check matching generate_invoice_number. Includes the settings UI field, sv/en strings, migration, and pg-real coverage. The CompanySettings type and Zod schema field for this feature landed earlier in 1bf3b641 (swept into the per-account VAT commit). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(dependabot): reduce open pull requests limit and group updates for better management --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
abe9ac9d8c |
Fix/attributes config (#926)
* fix(git): pin LF on generated extension registry and vitest snapshots setup:extensions and vitest write these files with LF; with core.autocrlf=true git expects CRLF and flags them as phantom modifications on every dev/build run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(security): enforce MFA on mcp-oauth consent and gate viewer storno route mcp-oauth/authorize renders an HTML consent page and issues 303 redirects that withRouteContext cannot express, so it kept raw getUser() and thereby skipped the AAL2 gate: a password-only (AAL1) session could approve consent that mints a long-lived, MFA-bypassing API key. Add a route-local requireAal2() step-up on GET and POST; AAL1 sessions redirect to /mfa/verify, BankID users are exempt. Separately, POST /api/reports/vat-declaration/rc-basis-gaps/fix calls correctEntry() (storno of a posted entry) but lacked requireWrite, so viewer-role members could trigger it. Add { requireWrite: true }. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): route transactions endpoints through withRouteContext Migrate the transactions routes off hand-rolled supabase.auth.getUser() onto the MFA-enforcing withRouteContext wrapper; add requireWrite on mutating handlers (book, uncategorize, attach-document, ignore, batch-match, create-from-document). Behavior and response shapes preserved; tests updated to the wrapper mock pattern. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): route SIE import and bank reconciliation through withRouteContext Migrate import/sie and reconciliation/bank routes onto the MFA-enforcing wrapper; requireWrite on mutations (import execute, create-accounts, mappings write verbs, link/unlink/run/mark-opening-balance). Reads (status, unmatched-entries) stay ungated. Response shapes preserved; tests added/updated to the wrapper mock pattern. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): route salary endpoints through withRouteContext Migrate salary employees and runs routes (plus ku, payroll-config, tax-tables) onto the MFA-enforcing wrapper; requireWrite on mutations. Personnummer masking/encryption untouched; file downloads (AGI XML, payslip PDF, payment files) keep their headers. Two payment-file GETs retain requireWrite because they stamp *_file_generated_at and previously gated viewers. Tests added/updated to the wrapper mock pattern. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): route report endpoints through withRouteContext Migrate the read-only report routes (trial balance, balansrapport, resultatrapport, income statement, ledgers, KPI, VAT declaration, salary journal, monthly breakdown, journal register, continuity check, full archive, etc.) onto the MFA-enforcing wrapper. All read-only, no requireWrite. JSON/XLSX/PDF/ZIP response bodies and headers preserved byte-for-byte; tests updated to the wrapper mock pattern. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): route invoices, skatteverket, agent and extension endpoints through withRouteContext Migrate invoices, supplier-invoices, skatteverket tax-payments, and dynamic extension routes onto the MFA-enforcing wrapper with requireWrite on mutations. The two NDJSON streaming agent routes (invoke, onboarding/stream) use requireAuth() directly (the wrapper can't wrap a streaming response) so MFA is still enforced. skatteverket payment-file GET keeps requireWrite (stamps a generated-at field). Response shapes and file headers preserved; tests added/updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): route documents, events, team and account endpoints through withRouteContext Migrate documents, events, kpi/preferences, vat/validate, support/contact onto the MFA-enforcing wrapper with requireWrite on mutations. account/password, team/accept and team/members use requireAuth() directly (user-level or pre-membership flows with no active company context) so MFA is still enforced. events keeps its dual API-key-or-session auth. Document retention guard untouched; tests added/updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): route settings and pending-operations endpoints through withRouteContext Migrate settings (api-keys, oauth-clients, booking-templates, counterparty-templates, logo, company settings) and pending-operations (commit, bulk-commit, reject, edit-before-approve) onto the MFA-enforcing wrapper with requireWrite on mutations. Credential-guarding routes keep their per-user ownership filters. Response shapes preserved; tests added/updated to the wrapper mock pattern. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(guards): ratchet raw-route-auth baseline 119->1 after A1 migration Lock in the withRouteContext migration so the count cannot regress. The single remaining entry, mcp-oauth/authorize, is a documented exception (HTML consent + redirects, MFA enforced via route-local step-up). Record the campaign and requireWrite decisions in DECISIONS.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(vat): add eSKD momsdeklaration file export for "Deklarera via fil" Generate the Skatteverket eSKDUpload v6.0 XML file so users can file VAT by upload instead of typing every ruta into the form. Extract buildFiledAmounts() as the shared whole-krona source of truth (öre truncated per SFL 22 kap 1 §) so the XML file and the manual-filing PDF can never disagree. Adds the /eskd API route, an XML option in the report export menu, and the upload button on the manual-filing card. Strings in sv + en. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(vat): add 'vat_settlement' source type and update related components * fix(booking): adjust search input layout and enable autofocus * fix(vat): support 12-digit org numbers and adjust emission order for eSKD file * fix(migration): add 'vat_settlement' to journal_entries.source_type CHECK --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
cbffcd7292 |
fix(invoices): accept empty self-billing fields on invoice create (#920)
#911 added self-billing fields to the shared CreateInvoiceSchema with external_invoice_number: z.string().min(1), but the invoice form has always sent that field (plus self_billing_agreement_ref and received_date) as '' on every normal invoice. The empty string failed min(1), so every invoice create returned 400. Normalise the empty optional self-billing strings to undefined in the schema (matching the existing optionalIsoDate / deduction_brf_org_number patterns), and strip the unused empty carriers client-side before the form POSTs. Required-when-self-billed is still enforced post-parse in the v1 route, so the self-billed path is unaffected. Adds schema regression tests. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3a88b53fd9 |
Add/api and invoice (#911)
* feat(salary): validate employee clearing/kontonummer at entry Bank details on the "Anställda" form had no structural validation, so a typo in clearing/kontonummer was saved silently and only surfaced at Bankgirot LB generation (or never, on the SEPA path). Adds a shared validator (lib/salary/payment/bank-account.ts) wired into the create dialog, edit page, CreateEmployeeSchema, and the PATCH route: 4-digit clearing or 5-digit Swedbank (8xxxx), 5-11 digit account, both-or-neither. Mirrors encodeReceiverAccount so entry-time validation matches what the payout layer can encode. Update validates only when a bank field actually changes, so legacy free-text data stays editable. Includes a conservative clearing to bank-name hint (null for unknown ranges). Per-bank mod10/mod11 checksum deferred to a soft-warning follow-up. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(chart-of-accounts): styled delete warnings and bulk select-all Replace the native window.confirm() on single-account delete with the styled DestructiveConfirmDialog, and add to the prune dialog a master 'select all unused accounts' checkbox plus an explicit confirmation step before bulk deletion. New sv/en strings for the confirm titles and actions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(salary): encrypt personnummer on v1 employee create; tolerate legacy plaintext on read The v1 REST create route stored personnummer unencrypted, which then threw ERR_CRYPTO_INVALID_AUTH_TAG on every decrypt-on-read path and 500'd the employees roster. Encrypt on write in v1 create, decrypt on read in the v1 list/detail/patch responses, and make decryptPersonnummer pass a raw 12-digit value through with a warn so a legacy plaintext row can't take the roster down. Encrypt seeded personnummer. Add a gated, idempotent backfill for existing rows. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(bookkeeping): save a manual entry as a reusable template Add a "Spara som mall" action to the manual journal-entry form next to the existing "Anvand mall" picker, so users can capture a booking pattern the moment they work it out. Opens the shared TemplateForm (create mode) pre-seeded from the current lines via deriveTemplateLinesFromBooking, and saves through the existing POST /api/settings/booking-templates. Rendered in both the mobile and desktop layouts and in create + edit modes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(pending): label all staged operation types The Granskning list rendered the raw snake_case operation_type (e.g. create_supplier_invoice_from_inbox) for any type missing from the label map, which hogs the meta row and wraps awkwardly on mobile. Add short sv/en labels for all operation types in OPERATION_RISK_TIERS, plus a humanized fallback for future ones, and simplify the label map to a plain operation_type -> i18n-key record (the icon/variant fields were dead). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(reports): let users file moms without a Skatteverket connection The momsdeklaration was never gated on the Skatteverket connection (it renders from the bookkeeping), but the not-connected "Anslut med BankID" card read as a wall. Make manual filing a first-class path: - Add a "Lämna in din momsdeklaration" card under the report with a PDF download (SKV 4700 layout, hela kronor) and a skatteverket.se link. - Add a momsdeklaration PDF route + template; buildManualFilingRows() rounds each ruta to whole kronor and recomputes ruta 49 per the SKV 4700 formula so it ties out. The PDF is a read/record copy, not a submission file (moms has no upload channel). - Offer PDF alongside Excel in the report's export menu. - Reframe the not-connected SkatteverketPanel to "Skicka direkt till Skatteverket (valfritt)". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(salary): compact new-employee dialog and warn on bad account check digit Redesign NewEmployeeDialog into a compact layout: borderless sections split by hairline dividers (no per-section cards), a fixed header + scrolling body + solid footer (fixes content showing through the old sticky bar), and denser grids. EmployeeTaxCard gains a `flat` variant so the dialog can host it without card chrome; the edit page keeps the boxed version. Add non-blocking Swedish account check-digit validation (lib/bankgiro/account-number.ts): mod10 (reuses luhn) + mod11, with a clearing->method table from the Bankgirot "Bankernas kontonummeruppbyggnad" spec, cross-checked against jop-io/kontonummer.js and verified against a real account (Forex 9420/4172385). Surfaced as a soft warning in both employee forms; unrecognised clearings return 'unknown' so we never warn on a valid but unmapped account. Never blocks saving. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(invoices): configurable send time + editing for recurring invoices Re-register the accidentally-removed recurring cron (now hourly) and add a per-schedule send hour (Europe/Stockholm, DST-aware). The cron never sends for a past date, and the enabling migration pauses every existing schedule on deploy so nothing auto-sends behind a user's back; users reactivate consciously (with a confirm) or click "Skapa faktura nu" to send this month on demand. Automatic sending now requires a customer email. Adds a full edit flow (row click opens the prefilled form, PATCH), fixing the row-click 404. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(invoices): configure självfaktura via the invoice API Add an optional is_self_billed flag (plus external_invoice_number, self_billing_agreement_ref, received_date) to the public invoice-create endpoint so callers can register a received self-billing invoice (mottagen självfaktura, ML 17 kap 15§) via the API. It was previously only reachable from the internal dashboard route, so it was missing from the API docs. Extract the booking into a shared service (lib/invoices/self-billed-sale.ts) and refactor the internal /api/invoices/self-billed route to a thin wrapper over it, so the dashboard and the API cannot drift. Books as a sale (Debit 1510 / Credit 30xx+26xx) with the counterparty's number; no own number is consumed. Fields are plain optionals (no schema refine) so UpdateInvoiceSchema.omit() keeps working; required-when-self-billed is enforced in the route. Documented in the endpoint registry. No migration (columns already exist). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(settings): allow a partial voucher-series-per-source-type map In Zod 4 an enum-keyed z.record is exhaustive (every source_type required), so saving a default_voucher_series_per_source_type map that omits a source type (e.g. the newly added result_appropriation) failed with "expected string, received undefined". Use partialRecord so the map can be sparse; the engine falls back to series 'A' for any unmapped key. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(salary): resolve employer name via getCompanyDisplayName Payslip PDFs, the payslip email, AGI, KU10, and the BG/LB + SEPA payment files now resolve the employer name through getCompanyDisplayName (company_settings.company_name, falling back to companies.name), matching how invoices already display it. Read-side coalesce, so no migration or backfill: companies.name is write-once at onboarding and not authoritative for these surfaces. The sidebar company switcher uses the same coalesce for the non-active companies in the list. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf(kontoplan): index-only account usage counts + lighter reference load Add a covering index on journal_entry_lines (journal_entry_id, account_number) so get_account_usage_counts becomes an index-only scan (prod worst case ~440ms). Slim /api/bookkeeping/accounts/reference to return only the company's activation rows and merge against the client-bundled BAS_REFERENCE instead of re-sending the full ~1,300-account catalog every load, and defer the BAS catalog + usage counts off the first-paint critical path in ChartOfAccountsManager. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * i18n(salary): add bank-account checksum warning string sv/en strings for the employee bank-account (clearing/kontonummer) soft checksum warning shown by the create/edit forms. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: update decision log Append the 2026-07-06/07 decision entries (salary employer-name coalesce, sidebar switcher, employees API personnummer fix, kontoplan load optimization, momsdeklaration manual filing, recurring invoices resend + reactivation + editing, "spara som mall", voucher-series partial map, and självfaktura via the invoice API). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: address compliance-review findings on recurring invoices + moms filing - recurring cron: close the double-send window with an atomic compare-and-set claim on last_run_at (release-on-failure) so two overlapping hourly runs can't both spawn from the same stale batch row - recurring edit dialog: force auto_send=false whenever the effective customer has no email, so a disabled-but-checked box can't PATCH auto_send=true after the async customer load - momsdeklaration manual-filing: truncate rutor to whole kronor (öretal faller bort per SFL 22 kap 1 §) instead of round-to-nearest, matching the SRU path Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
cdac1808c9 |
feat(api): ROT/RUT, articles, and project lifecycle on the v1 API (#904)
* feat(api): ROT/RUT + articles + dimensions on the v1 invoice surface (#895) - v1 invoice POST now routes through buildInvoiceWriteData, the same builder as the dashboard: ROT/RUT deduction lines (server-side compute, personnummer encryption), article_id + revenue_account linkage, accruals, and line_type no longer get silently dropped on the wire. - v1 invoice PATCH accepts default_dimensions so integrations can tag a draft with a project/cost centre after creation. - New PATCH/DELETE /dimensions/:id/values/:valueId: rename, archive, set end_date on project codes; delete unreferenced values (409 with an archive hint when the BFL retention trigger blocks). - New GET /articles: read-only artikelregister list (incl. housework_type) so callers can resolve article_id before composing invoice lines. - Invoice GET/POST projections now expose deduction fields and full item columns; dry-run previews never echo the encrypted personnummer. Closes #895 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): address review on #904 - Extract shared v1 invoice projections to lib/api/v1/invoice-columns.ts so create/detail/patch responses can't drift; PATCH now returns deduction_total + deduction_personnummer_last4 like GET/POST. - Narrow the v1 create customer fetch back to the three fields the builder reads instead of select('*'). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
2c2743eb79 |
Check/salary bankid api (#892)
* fix(bankid): harden login/signup flow — polling, signup rollback, metadata merge, enrichment lookup - middleware: read BankID enrichment from the bankid_enrichment table (the extension_data path has been dead since the multi-tenant refactor), so company-less BankID users land on /select-company instead of the manual wizard - BankIdAuth: hard 6-min poll deadline; every failed poll counts toward the give-up limit; guard overlapping ticks so completion runs exactly once (a double /complete regenerated the magic link and invalidated the first, failing logins intermittently); retry clicks wait out the start cooldown instead of silently no-oping; Swedish messages for 429/unknown start errors - bankid/complete: all-or-nothing signup — delete the created user when the identity insert, app_metadata update, or magic-link generation fails, so a retry starts clean instead of hitting account_exists with an unusable account - bankid/unlink: read-merge-write app_metadata so has_password survives unlink (BankID-only users could otherwise strand themselves with no login method) - login: BankID "create account" CTA now links to /register instead of dismissing the notice; sv.json: fix missing å/ä/ö in settings_bankid strings Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: move secondary guides into docs/, delete dead root files Move DOCKER.md, SELF-HOSTING.md, WHITELABEL.md and extensions.md (renamed EXTENSIONS.md) into a new docs/ folder and update all path references (README, setup.sh, .dockerignore image rules, docker-publish workflow comment, _example-branding, lib/branding/service.ts). Delete two dead root files: customer.json (stray API-test payload) and findings.md (point-in-time swarm audit export, criticals already filed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Emil <emilmattsson14@gmail.com> * fix(api): security & correctness hardening + withRouteContext MFA migration across API routes Audit of ~100 app/api routes. Highlights: Security - agent/conversations: list leaked colleagues' titles + message previews (company-scoped RLS, no user filter) -> user-scoped - calendar/feed PUT: raw body into .update() allowed feed_token fixation on a public unauthenticated URL -> strict schema, content toggles only - bokslutsdispositioner: unbounded schablonintaktRate could inflate the IL 30 kap 25% periodiseringsfond cap base -> bounded - agent profile/composer/onboarding: viewers could rewrite the agent profile while sibling /verify blocked them -> role-gated Correctness - account-totals / listAssets: unbounded queries silently truncated at 1000 rows (under-counted money; skipped assets at year-end depreciation) -> fetchAllRows with stable order (+3 more pagination fixes) - voucher-gaps: swallowed detect_voucher_gaps RPC errors (BFNAR gap view could show "no gaps" when the check never ran) -> surfaced - 5 phantom-success writes (OK on zero matched rows) fixed - assets K3 component-sum validated against stale acquisition_cost -> fixed - invite silent email-send failure -> response carries email_sent; deadlines/calendar cast-then-check JSON crashes -> Zod Convention - ~44 legacy routes converted to withRouteContext (MFA); added Zod validation, corrected status codes, console.* -> lib/logger Response shapes preserved for existing callers. ~110 new tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(bookkeeping): save a booking as a reusable template from Bokför direkt Add a "Spara som mall" action to the manual booking dialog so users can capture a kontering they just worked out as a booking template — right where they figured out how something should be booked. - derive amount-parameterised template lines from the concrete booking (settlement = the non-VAT leg nearest the total, 26xx = a VAT line with its rate snapped to the nearest standard rate, the rest = business ratios; line labels come from the loaded BAS chart) - extract the shared TemplateForm out of BookingTemplatesPanel so the booking dialog reuses the same editor, live preview and convertibility hints instead of duplicating them - save via the existing POST /api/settings/booking-templates endpoint Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(bokslut): render arsredovisning RR/BR at ÅRL post level — no kontonummer Bolagsverket rejected a user's filed årsredovisning with "Balansräkning och resultaträkning ska inte innehålla kontonummer": the PDF built every statement row as per-account "1930 Företagskonto" lines while the iXBRL filing path already aggregated to statutory posts, so the two artifacts diverged. The PDF statements now derive from the same K2 risbs mapping the iXBRL document uses (mapTrialBalancesToK2), via a new statement-rows.ts that emits post-level rows in uppställningsform order for both the K2 and K3 templates. Also fixed along the way: - Jämförelseår column (ÅRL 3:5 §) — previous-year trial balances now load and render; the old PDF had no comparatives at all. - mapping.warnings (unmapped accounts, RR ≠ 2099, obalans, reclass nudges) flow into ArsredovisningData.warnings so the wizard flags a non-fileable document before download. - Flerårsöversikt current/previous year overridden with the mapper's strict-3000–3799 Nettoomsattning, mirroring build-input's duplicate-fact rule, so the FB table ties to the RR. - FB eget kapital-table is post-level and drops obeskattade reserver (never eget kapital); K3 equity-changes statement uses real prior-year opening balances with derived utdelning/nyemission residuals that tie the roll-forward exactly to booked UB. - build-input dedupes warnings now that the PDF path runs the same mapping. Regression test asserts no RR/BR label ever contains a four-digit account number again. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reports): diagnose untransferred prior-year results behind balance-sheet differens Prod incident (97 kr): a multi-year SIE migration lacked one year's omforing av arets resultat; the residual corrupted every later derived opening balance and Balansrakningen showed a bare "Differens: 97 kr" with no explanation. Continuity checking cannot catch this failure mode (prior-year UB and derived IB match per-account by construction) - the invariant that actually breaks is per-year P&L = 0 for all non-latest years. - lib/reports/imbalance-diagnosis.ts: shared detector (findUntransferredResults + buildImbalanceDiagnosis) - Balansrakning/Balansrapport attach imbalance_diagnosis when unbalanced, naming the exact culprit years; rendered in web views + PDF; MCP gnubok_get_balance_sheet inherits the field via spread - SIE import: parse-time warning when a completed year's vouchers leave a P&L residual, plus a post-import DB walk surfacing culprits as warnings and structured details.untransferredResults; the Arcim migration workspace previously dropped result.warnings entirely and now renders them - opening-balance/correct: pre-flight the company lock date and return 409 OB_COMPANY_LOCK_DATE (retryable: false, lock date interpolated in the client message) instead of the retryable 500 that invited blind retries; catch-path maps a raced trigger rejection to the same code Diagnosis runs only on unbalanced paths (zero cost when healthy) and never fails the report or the import. No migration, nothing persisted. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: production error remediation — FX rates, deadlines, log levels, correction relink Batch of fixes for recurring Vercel runtime errors: - Riksbanken FX rates: persistent read-through cache (exchange_rates table), one retry honoring Retry-After on 429/5xx, bounded ingest concurrency, and an honest fallback — most recent cached observation or null, never a hardcoded rate silently booked into amount_sek. Unrated transactions stay repairable via refresh-exchange-rate. - Tax deadline regeneration inserts replacement rows before deleting the superseded set, so a failed insert no longer wipes a company's deadlines (the 23502 user_id regression did exactly that). Migration makes deadlines.user_id nullable for system-generated rows. - Route wrappers + errorResponse log 4xx outcomes at warn so only genuine 5xx reach Vercel's runtime-error clustering; client-supplied /api/log telemetry demoted to warn as well. - application/json documents (raw PSD2 responses archived per BFL) validate as parseable JSON with object/array root instead of always failing the magic-byte check. - correctEntry surfaces document-relink failures to callers, and the BFL document-immutability trigger now allows relinking underlag from a reversed entry to its correction (migration + pg test). - Middleware clears stale session cookies on /api requests too, using scope 'local' so cleanup doesn't re-trigger the failed token refresh. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(skatteverket): persist token health and stop retrying dead consents Terminal auth errors (SESSION_EXPIRED, REFRESH_EXHAUSTED, MISSING_SCOPE, TOKEN_CORRUPTED) mark the token row needs_reconsent with the error code and timestamp — SKV per-flow refresh tokens live 65 minutes, so once expired nothing recovers without a fresh BankID consent. The AGI kvittens and skattekonto sync crons skip flagged connections instead of failing every night, and the settings panel prompts for re-consent proactively. A successful reconnect resets the row to active. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(banking): allocate distinct BAS ledger slots for PSD2 mirror accounts A bank returning N same-currency accounts used to map them all onto the currency default (1930/1932/1933/1934), tripping the UNIQUE (company_id, ledger_account) constraint per-account — swallowed errors left accounts silently unmirrored. allocatePsd2LedgerAccount now hands out the currency default first, then free 1931–1959 sub-account slots, skipping slots held by any existing row. - Callback persists allocations to accounts_data so the picker pre-fills reality; reconnect reuses previously mirrored ledgers instead of re-deriving (a user remap to 1935 survives). - Selection save resolves effective ledgers up front and rejects duplicates or cross-connection conflicts with a 400 instead of silently skipping the mirror. - Bank error codes + psu_type are forwarded to the settings page for every OAuth error, keying the Handelsbanken corporate fullmakt guidance. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent): stage exact journal lines on categorization previews Categorization previews only carried debit/credit accounts, the GROSS amount, and separate VAT rows — read together that looks like an unbalanced 'gross on cost account + VAT debit' entry, and it misled both users and agents into rejecting correct proposals. The MCP preview and the pending-operation PATCH now materialize the exact lines the commit executor will post (net cost line, VAT line, gross bank line, SEK) via buildTransactionEntryLines, and PATCH re-derives them from the new mapping instead of spreading stale staged lines. ApprovalCard and /pending render the verifikat lines, falling back to the legacy summary only for operations staged before this fix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(bookkeeping): prune unused imported accounts from the chart SIE imports routinely bring in hundreds of accounts that were never used and clutter the kontoplan. New account_usage_counts RPC (one grouped query instead of a count per account) backs GET /api/bookkeeping/accounts/usage, and POST /api/bookkeeping/accounts/prune deletes zero-usage accounts — dry-run first, then an explicit account list capped at 2000. Accounts with journal lines are skipped, never deleted. The chart manager shows a usage column and a prune dialog grouping custom accounts vs unused BAS-seeded ones. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(api): carry dimensions through v1 invoice and supplier-invoice surfaces Credit-note creation now copies default_dimensions and per-line dimensions from the original, so the reversing journal entry nets against the same dimension cells instead of dropping them. List/detail responses expose the dimension fields, and the OpenAPI spec snapshot follows. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf: batch serial Supabase round-trips on hot dashboard paths Every dashboard render pays the layout's query chain, so serialized awaits are direct wall-clock: the layout, chat conversation, invoice detail, supplier detail, select-company, and agent-onboarding pages now run their independent lookups in parallel batches, and getCompanyCapabilities folds its disabled-config read into the same round-trip. JournalEntryList hydrates the saved fiscal-year scope optimistically instead of serializing the first entries fetch behind the fiscal-periods request. The supplier detail page filters invoices server-side via a new supplier_id query param instead of fetching the whole company ledger, and the invoice editor (with its framer-motion dependency) lazy-loads so it stops shipping with the invoice list bundle. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(salary): one-click runs, payslip delivery, payments settings, run cockpit Salary P1 batch, driving the 20-click flow toward 3 clicks: - One-click 'Starta lönekörning': POST /api/salary/runs accepts an empty body and resolves defaults server-side — period follows the latest non-corrected run, payment date from the new salary_pay_day setting, series from the per-source-type map. The separate /salary/runs/new page is gone. - Run detail page rebuilt as a step-railed cockpit (progress rail, KPI cards, employee ledger, journal preview) on a deliberately wider canvas; components extracted to components/salary/run/. - Payslip delivery: tokenized public payslip pages (/payslip/[token], backed by salary_payslip_links) plus per-employee email send with PDF — employees need no account, and the middleware exempts the route from auth redirects. - Payments settings: salary pay day, default bank, and pain.001 vs Bankgirot Lön format with per-bank upload instructions and an LB sunset warning (banks retire LB during 2026). - AGI panel: full submission status flows (stale drafts, signing links, kvittens polling, error reports); tax payment panel with skattekonto shortcut and mark-as-paid. - Salary calendar bulk editing, employee benefits/tax-card polish, municipality tax-table lookup improvements. messages/sv+en also carry the strings for the account-prune, skatteverket-reconsent, and banking surfaces committed just before this. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: adopt Next 16 proxy.ts convention + repo housekeeping - Rename middleware.ts to proxy.ts with the proxy() export (Next 16 renamed the middleware convention; behavior unchanged). - Exclude dev_docs/ from tsconfig so stray snippets in planning docs don't break the build type-check. - Ratchet antipatterns-baseline down (raw-route-auth 165 → 119) to lock in the withRouteContext migration from 5cfd2b76. - template-library uses roundOre() instead of inline rounding. - database.md: drop account_balances from the key-tables list. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(bookkeeping): robust service-role detection in correction document relink relink_documents_to_correction() keyed its service-role branch on auth.role(), which reads the singular request.jwt.claim.role GUC that PostgREST v10+ and the pg-real harness no longer populate. Genuine service-role callers (pending-ops executor / MCP approve) landed in the auth gate and could not relink underlag. Read the role from the request.jwt.claims JSON directly, mirroring the canonical link_voucher_rpcs_tenant_guard convention. Validated on staging. Also: harden the salary run page's error paths (res.json().catch) against non-JSON error bodies, and roll back the pg-real service-role case in finally so an aborted transaction cannot poison a pooled connection for the next test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(documents): restore journal_entry_line_id link durability (BFL 7 kap) Migration 20260704103000 rewrote enforce_document_journal_entry_immutability to guard journal_entry_id but left journal_entry_line_id to the metadata trigger, which exempts draft-linked docs -- and the entry-level trigger only fired on UPDATE OF journal_entry_id, so a line-id-only UPDATE never invoked it at all. That let a set journal_entry_line_id be cleared to NULL, breaking the "link durable from first set" invariant (document-immutability.pg regression). Widen the trigger to fire on journal_entry_line_id too and guard it with the same uuid-durability rule as journal_entry_id (setting NULL -> uuid stays allowed; clearing/re-pointing a set value is blocked, status-independent). The correction-relink GUC path, which legitimately clears line_id when moving underlag to the posted correction, stays exempt. Validated on staging. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Signed-off-by: Emil <emilmattsson14@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
764348e99c |
feat(dimensions): PR10 advanced — custom dimensions, hierarchy, account rules, commit enforcement (#886)
* feat(dimensions): PR10 advanced — custom dimensions, hierarchy, account rules, commit enforcement The final rung of the dimensions ladder (dev_docs/dimensions_implementation_plan.md §7 row 10): - custom dimensions: POST /api/dimensions creates registry dims (next free SIE number >= 20 when omitted; explicit numbers allowed — SIE import already mints reserved ones); register gets a 'Ny dimension' dialog with a quiet Avancerat disclosure for the #UNDERDIM parent; GET now carries parent_sie_dim_no (the column + SIE round-trip existed since PR1/PR5 — this exposes it) - account_dimension_rules (migration 20260703120000): one rule per (account, dimension) — required / default / fixed, per-rule is_active, company-scoped RLS, composite FK to the registry, value-presence CHECK - enforcement, opt-in BY CONSTRUCTION (zero rules = engine byte-identical; deliberately NO settings toggle — a rule that exists but is ignored is worse than either extreme): default/fixed apply onto line bags at draft creation (fixed overwrites, default fills); required asserts at commitEntry with a Swedish MANDATORY_DIMENSION_MISSING naming every account + dimension; the bulk-book route runs the same policy before its RPC; storno/correction paths never pass through commitEntry so history always reverses regardless of policy; rule fetches fail open incl. thrown exceptions - chart of accounts: per-account Dimensionsregler section in EditAccountDialog (Krävs/Förval/Låst, value picker, pause switch), gated on the existing dimensions toggle, quiet when empty - pickers: LineDimensionFields is registry-driven (one combobox per active dimension, cached fetch, hardcoded 1/6 fallback) — every existing mount lights up custom dims with zero changes - agent briefing: per-dimension required_on_accounts/default_on_accounts so agents self-correct instead of bouncing off the policy error - rules CRUD API with existence/active/company validation and qualified DTO ids; firm_id FK deferred until the firms table lands (per plan) 39 new tests (pure-fn rules, engine enforcement, both new API surfaces, pg-real RLS/CHECK/cascade suite); full suite 6,791 green; migration replayed on a fresh container. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: renumber migration to 20260703200000 — version collision with prod The concurrent session shipped pending_operations_add_link_document_to_voucher as 20260703120000 today; the Supabase preview branch (cloned from prod) rejected the duplicate version key. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: review round — auto-pick retry on collision, fail-open warnings, query schema - POST /api/dimensions retries once past a concurrent number claim when the number was auto-picked (explicit choices still 409) - every fail-open skip of the dimension-rules policy now logs a structured warning (engine draft/commit paths + bulk-book) — deliberate fail-open, but observable - GET /api/dimensions/rules validates its query through ListDimensionRulesQuerySchema instead of an inline regex Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |