c0ecf2fa3bebd46bdfd0169efd73b89653d1dfed
27 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ea45e9dc2f |
fix(invoices): say what payment detail is missing, per currency (#2126) (#2139)
"Fakturan saknar ett betalningskonto för vald valuta" read as a foreign-currency account when the invoice was in SEK and the gap was simply the company's bankgiro; the remediation line also asked for an IBAN, which SEK does not need. A Visma-migrated user marking invoices as sent hit this and went looking for a valutakonto. - describeMissingInvoicePaymentAccount(currency) in lib/invoices/payment-accounts.ts: SEK names bankgiro, plusgiro, Swish or bank account; other currencies ask for an IBAN account in that currency (USD/GBP also offer routing number / sort code + BIC). Both point at Inställningar → Fakturering. - getErrorMessage branches on INVOICE_SEND_PAYMENT_ACCOUNT_MISSING + details.currency (every dashboard route already sends it), before the English registry shortcut so both locales get the specific text. - Registry entry rewritten currency-neutral for consumers without details (API, MCP): bankgiro/plusgiro/Swish/bankkonto for SEK, IBAN otherwise; remediation no longer says IBAN for everything. - Staged-operation commit path uses the helper directly. Tests: helper per currency, client mapping sv/en and the no-details fallback. Closes #2126 Claude-Session: https://claude.ai/code/session_01WFhSQWzu5SyXB6kG5ActZc Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
0406e628e1 |
fix(settings): scope cross-field VAT validations to saves that touch them (#2121)
* fix(settings): scope cross-field VAT validations to saves that touch them
The settings PUT validated the whole effective record on every partial
update, so companies stored as vat_registered without a vat_number were
blocked from saving anything through the endpoint, including the invoice
bank-details dialog, which has no VAT fields (reported by a user stuck on
"Momsregistreringsnummer kravs...").
Each cross-field check (VAT completeness, 40m-monthly, periodisk
sammanstallning) now runs only when the request body touches a field in
its group, so the invariant still holds whenever VAT config is edited.
Explicit null now counts as clearing a value during validation instead of
falling back to the stored one, closing a latent hole where
{ vat_number: null } passed validation but wrote null.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016fjJLUucErb1ZHyQ57fe1u
* fix(invoices): gate issuance on the seller VAT number (skeptic finding)
The settings scoping in the previous commit removed what was accidentally
the only enforcement of "momsregistrerad implies momsregnr on file": with
bank details saveable again, a registered company without a stored VAT
number could issue a faktura charging moms with no seller VAT number in
the footer (mandatory element, ML (2023:200) 17 kap. 24 §).
Issuance is now gated the same way the payment account is, at all four
independent issuance points (issueAndBookInvoice, dashboard send, v1 send,
v1 mark-sent), with a structured error pointing at Installningar -> Skatt.
Credit notes, proformas, and delivery notes are exempt like the payment
gate exempts them.
Also, per the Swedish review and the secondary skeptic finding:
- PS/EU-trade edits join the VAT-completeness touch group, so enabling
periodisk sammanstallning on an incomplete registration keeps failing.
- The stale ML 11 kap. 8 citation is updated to ML 17 kap. 24.
The makeCompanySettings fixture now models a coherent registered company
(vat_number set); the missing-number tests override it explicitly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016fjJLUucErb1ZHyQ57fe1u
* fix(invoices): extend the seller-VAT-number gate to the headless issuance paths
Skeptic round 2 found three more issuance points beside the four gated in
the previous commit: the recurring auto-send service (cron, no human in
the loop), and the MCP staged-operation executors send_invoice and
mark_invoice_sent. Each carried the payment-account gate but not the VAT
gate; mark_invoice_sent additionally had a narrow settings select that
would have made a naive gate silently pass, now widened.
Recurring auto-send fails soft, matching its other guards: the invoice
stays a numbered draft with the standard schedule warning. The executors
return the structured Swedish message. Peppol send was verified
self-gating (BIS preflight requires the supplier VAT number).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016fjJLUucErb1ZHyQ57fe1u
* test(email): refresh brand-mail snapshots for the coherent VAT fixture
The makeCompanySettings fixture now carries a VAT number, so the invoice
and reminder mail footers correctly render the VAT line; the snapshots
predate that. Also cites ML 17 kap. 22-23 (andringsfaktura content list)
in the seller-vat-number docstring per the Swedish review suggestion,
documenting why credit notes are exempt. No behavior change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016fjJLUucErb1ZHyQ57fe1u
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
f1d76deaba |
fix(providers): stop dead-ending on a resource 403, and stop dropping every migrated kreditfaktura (#2113)
* fix(providers): stop dead-ending on a resource 403, and stop dropping every migrated kreditfaktura
Two independent defects in the provider migration, both customer-visible.
A per-resource 403 was classified as a dead grant. classifyProviderError mapped
any 401 or 403 to PROVIDER_AUTH_EXPIRED, which is fatal, so a Fortnox account
without leverantorsregister permission aborted the whole migration at the
suppliers step with "Anslutningen har gatt ut. Ateranslut" even though the same
token had just succeeded on the previous step. Reconnecting can never fix that,
and steps 4 and later never ran. The provider's own reason ("Saknar behorighet
for leverantorsregister.") never reached the user. A 403 is now non-fatal once
the same token has already succeeded in the run, the migration continues, and
the provider's reason is surfaced. A 401, or a 403 on the first call, keeps the
auth-expired path.
fetchCompanyInfoDirect swallowed every error and returned null, which made the
existing PROVIDER_API_MODULE_INACTIVE remediation unreachable: a Visma customer
whose api_standard module is off got a silent 200 with an empty company card
instead of the precise Swedish explanation that was already written.
Kreditfakturor were dropped entirely. entity-mapper wrote document_type
'credit_note', but invoices_document_type_check allows only invoice, proforma
and delivery_note, and credit notes are modelled by credited_invoice_id. Every
migrated kreditfaktura was rejected and counted as skipped. One customer
imported 255 sales invoices and 0 credit notes on 2026-08-31; AR and revenue
are overstated by the credited amounts, and kreditfakturor are
rakenskapsinformation. They now import as invoice rows with reversed amounts
and status 'credited', following the in-app credit convention. They import
unlinked: no provider DTO carries a reference to the invoice being credited, so
there is nothing to match on and guessing would corrupt the AR ledger. The
wizard says so instead of burying them in skipped.
Also makes the OAuth callback non-replayable from browser history (no-store
plus history replacement), which is what the "state rejected" events were: a
replay of a callback that had already succeeded seconds earlier. No
already-connected page, so consumed-vs-unknown state stays unobservable to an
unauthenticated caller. Expected PSD2 session expiry drops from error to warn.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ifKg6Ec67A39oxfGPU1yc
* fix(arcim): entity line needs the failed flag
The unlinked-credit-note row omitted `failed`, which the entityLines element
type requires. Caught by the zero-extensions build, not by vitest: the unit
suite does not typecheck.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ifKg6Ec67A39oxfGPU1yc
* fix(arcim): write the missing-reference disclosure onto the credit note itself
Review finding (swedish-compliance-review-bot): ML 17 kap 22-23 § wants a
kreditfaktura to reference the invoice it credits, and BFL 5 kap 6-7 § wants a
verifikation to reference its underlag. No provider DTO carries that reference,
so the pairing cannot be resolved at import and guessing it would corrupt the
AR ledger. Reporting the count in the migration wizard is not enough: a result
screen is not rakenskapsinformation, and the gap has to be legible on the
record itself years later.
The disclosure now goes into invoices.notes and supplier_invoices.notes,
preserving whatever note the provider sent.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ifKg6Ec67A39oxfGPU1yc
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
4b7343d5ec |
fix(errors): keep the SQLSTATE when wrapping database errors (#2027)
isTransientFailure() checks the driver's error code first, and 57014
(statement timeout) is already in its transient set. But the wrapping idiom
across the codebase was `throw new Error(\`Database error: ${err.message}\`)`,
which keeps the prose and drops the code. A retryable timeout therefore
arrived anonymous and resolved to UNKNOWN_ERROR: "Något gick fel. Försök
igen." An agent cannot dispatch on that, so it retried.
On production over 60 days, with the two bot integrations excluded: 1024 real
agent failures, 645 of them UNKNOWN_ERROR across 60 actors and 57 companies.
82 retry streaks of three or more identical failures, 462 wasted repeat calls,
53.1% of all agent error calls sitting inside a streak.
The worst offender traces to one line in core. gnubok_query_journal failed 164
times at a p50 of 8110ms while every other failing tool sat between 1 and
315ms, and its path is fetchEntryLines -> fetchAllRows, where
lib/supabase/fetch-all.ts threw `new Error(error.message)`. That is the
highest-traffic strip point in the repo: 31 callers, every paginated read.
query_journal already had a correct TRANSIENT_ERROR branch offering "retry, or
narrow with date_from/date_to" which could never fire, because by the time it
looked, the code was gone.
fetch-all keeps the driver message verbatim: callers match on the existing
text, and this adds the code rather than rewording anything.
Attaching the code is safe. extractCode() only accepts /^[A-Z_]+$/ and every
SQLSTATE contains digits, so it cannot be mistaken for one of our own stable
codes. There is a test for that, and one asserting the old bare-Error shape
still resolves to UNKNOWN_ERROR so the fix cannot silently regress.
Also stops rendering the literal "undefined" when a driver-level failure
carries no message, which is the string that made these unsearchable.
Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
ca93ef3fb6 |
fix(salary): surface employee-save failures and a typed 503 for the missing encryption key (#1996) (#2009)
* fix(salary): surface employee-save failures in the dialog and type the missing encryption key (#1996) Pressing Spara in "Ny anställd" could fail without any feedback: a thrown fetch or a non-JSON 5xx body escaped handleSubmit before setSaving(false) ran, leaving the button stuck on "Sparar..." and the dialog silent. Even when the toast did fire, the Radix modal aria-hides the root-layout Toaster, so assistive tech (and the E2E driver that found this) heard nothing, and the requestId support needs was never shown anywhere. - NewEmployeeDialog: fetch + parse run in a never-throwing helper, saving is released in finally, the body is parsed with json().catch(() => null) so an HTML/plain-text error page still maps through the HTTP-status map, and the failure is rendered inline (role="alert" in the footer) with "Ärende-id: <requestId>" next to the single destructive toast. - personnummer.ts: the production "key missing" throw now carries the registry code PERSONNUMMER_ENCRYPTION_NOT_CONFIGURED, and the SALARY registry gains a 503 entry naming PERSONNUMMER_ENCRYPTION_KEY with a "contact support" message and a remediation hint. withRouteContext emits the typed envelope automatically instead of INTERNAL_ERROR 500, which read as transient and invited retries that can never succeed. - Tests for the route (401, 400, 503 with requestId and no insert), the key guard, the registry entry, errorResponse dispatch on a coded Error, and getErrorMessage locale handling of the new envelope. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015nAd8XJ2RPCmG2eKoLBdna * fix(salary): address review findings (#1996) - NewEmployeeDialog: fall back to the X-Request-Id response header when the body carries no error.requestId. The route hand-builds its 409 (duplicate personnummer) and generic insert-failure 500 bodies as flat strings, so the inline "Ärende-id" line was hidden for exactly the DB-failure class the issue names; withRouteContext sets the header on every response. - Route tests pin that the 409 and 500 insert-error arms carry X-Request-Id. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015nAd8XJ2RPCmG2eKoLBdna --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
64119d30bc |
fix(bank): Swedish provider errors and a durable failure trail for bank connect attempts (#1841)
Issue #1716: a user stuck in Handelsbankens fullmakt step got a raw provider token back (server_error, invalid_state) and support had nothing to look at afterwards: the failed pending row is deleted by design, the callback only logged to console (short retention), and event_log recorded successes only. Diagnosis of the reported case: the failures were on the bank's side (the corporate fullmakt requirement); both of the reporter's companies connected successfully on 2026-08-12 with no code change on our side in between, and the connections have been active and syncing since. Changes: - lib/errors/get-error-message.ts: getBankConnectionErrorMessage() maps PSD2 callback outcomes (access_denied, server_error, temporarily_unavailable, session expiry, plus the internal invalid_state, missing_parameters and invalid_code_format tokens) to Swedish user messages, appending the raw provider description so the underlying error is still surfaced. - callback route: every bank_error redirect and the stored error_message now carry the mapped Swedish text; bank_error_code, bank_name and psu_type still flow so the settings page keeps its targeted guidance (Handelsbanken fullmakt steps included). - New audit events bank_connection.consent_denied and bank_connection.finalize_failed are emitted on the two failure paths and persisted to event_log, so support can answer which attempt failed, with which provider error, on whose side, even after the row is gone. Claude-Session: https://claude.ai/code/session_01SyDuePXxUFowaPBKpAv8SF Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d35c401c0c |
fix(mcp): over-long reason gets VALIDATION_ERROR and a specific Swedish message (#1811)
* fix(mcp): over-long reason gets VALIDATION_ERROR and a specific Swedish message gnubok_reverse_journal_entry / gnubok_undo_sie_import cap `reason` at 500 characters, but exceeding it produced code UNKNOWN_ERROR with message_sv "Något gick fel. Försök igen." while the cause sat only in message_en. getStructuredError now infers VALIDATION_ERROR from the message and getErrorMessage maps it to "Motiveringen får vara högst 500 tecken.". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(errors): pin the reason-length pattern to 500 and assert the envelope on undo_sie_import Review findings: the Swedish message hard-codes 500, so the pattern must match that limit only; the undo_sie_import 501-char test now asserts code and both localized messages like the reverse test does. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
9622382579 |
fix(mcp): surface the database reason and code behind LINK_TX_DB_ERROR to the approver (#1807)
* fix(mcp): surface the database reason and code behind LINK_TX_DB_ERROR to the approver gnubok_link_transaction_to_journal_entry failed reproducibly for a customer on certain incoming payments with a bare LINK_TX_DB_ERROR: the service put the Postgres message in details.reason, but the code had no structured entry and the commit dispatcher dropped executor data on failure, so neither the MCP approve result nor result_data said why. LINK_TX_DB_ERROR now has a structured entry; the executor appends the DB reason to the message and sets errorCode; the dispatcher persists and returns executor failure details (result_data.details, CommitResult.data, .code); gnubok_approve_pending_operation exposes error_code. The next failing call tells us which constraint or trigger fired. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(mcp): keep tools/list under the context budget (drop approve schema descriptions) The two output-schema descriptions added for data/error_code pushed the projected tools/list payload 7 tokens over the ceiling guarded by payload-size.bench.test.ts. The fields stay; the prose goes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(pending-ops): log loudly when the terminal rejected write fails Review finding: the rejection branch wrote pending_operations without checking the result, so a failed write left the row in 'committing' with the executor error, code and details lost silently. Mirror the finalize branch: inspect the write result and log with the ids plus the failure we could not persist; the daily recovery sweep still resolves the row. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
43cde6deb9 |
fix: unignore transactions during categorization (#1683)
Fixes #1660 |
||
|
|
3ec76d39db |
fix(providers): correct Bokio v1 connection validation (#1681)
Fixes #1670 |
||
|
|
07e89d9b52 |
feat(invoices): add Peppol delivery foundation (#1595)
* feat(invoices): add Peppol delivery foundation * fix(invoices): harden Peppol compliance guards * fix(api): narrow Peppol document loading * test(pg): hash Peppol fixture payload * fix(invoices): address Peppol review findings * test(pg): isolate Peppol provider events * test(pg): isolate Peppol submission fixtures |
||
|
|
38a890c8d1 |
fix(underlag): carry the phone photo that is too big to send, and say why when we cannot (#1550)
* fix(whatsapp-inbox): register the channel question event types Every follow-up question the WhatsApp intake asks has been failing its processing_history append in production: ChannelQuestionAsked, ChannelQuestionAnswered and ChannelQuestionExpired were never added to the processing_event_types catalog the event_type FK points at. appendQuestionHistory() catches and logs that failure by design, so the reply to the sender still goes out and nothing looked broken from the outside. What was lost is the durable record of the exchange, which is part of how the underlag was obtained (BFNAR 2013:2 kap 8). Catalog rows only: aggregate_type 'System' already passes the CHECK. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(underlag): say why an upload failed, and get out of an expired session A user reported that none of the three ways to add a receipt from a phone worked, all of them answering "Uppladdning misslyckades. Nagot gick fel, forsok igen" immediately. Production told us nothing: every upload request that reached the route in the same 24 hours returned 200. Both halves of that are the same bug. The workspace read failures as `throw new Error(json.error)`, which loses a body that is not JSON (the res.json() call throws first) and stringifies the structured envelope to "[object Object]", so anything the route did not answer with a plain string arrived as the generic fallback. The middleware 401 for an expired cookie session is exactly that envelope shape, and a phone tab left open is exactly where the session expires unnoticed: the controller's timers are throttled in the background, so the request the user just made is what finds out. Now the response is resolved where it fails, through the house helper that already knows the status map, and an expired session is announced on the session-timeout BroadcastChannel so the controller signs out and routes to /login the same way it does for an expired heartbeat. Failed uploads also post metadata (status, size, mime type, resolved reason) to /api/log, the one API path exempt from the timeout gate, so a request answered before the route runs stops being invisible. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(underlag): carry the phone photo that is too big to send The reported failure was not the account and not the session: hosted rejects any request body over 4.5 MB itself, before the function runs. Measured against production, 4.4 MB reaches the route and 4.6 MB comes back as a plain-text FUNCTION_PAYLOAD_TOO_LARGE. Nothing invokes the function, so nothing lands in the logs, which is why one user's failing uploads were invisible while every upload that arrived returned 200. An iPhone photo in "Most Compatible" mode is 4-12 MB, so whether it worked depended on whose phone took the picture. Meanwhile the route advertises a 10 MB limit it can never be handed. Photos are now re-encoded in the browser when they exceed what the platform will carry: 2400px on the long edge at JPEG q0.85, stepping the quality down only if that is not enough. That keeps the small print on a receipt legible, which is what BFL 7 kap asks of an archived underlag ("varaktigt läsbart skick", a faithful reproduction), and a refusal is not. What cannot be shrunk (a PDF, or HEIC where the browser will not decode it) is refused before the upload starts, naming its actual size and the limit rather than failing in transit. 413 joins the HTTP status map so a rejection we cannot pre-empt still says what happened: the platform's body is plain text, so the status is the only thing there is to translate. Self-hosted Docker has no proxy in front of the app, so none of this applies there and the route's own MAX_FILE_SIZE keeps governing. 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> |
||
|
|
63d520719a |
fix(import): treat a voucher-less SIE file as a no-op, not a failed migration (#1445)
* fix(import): treat a voucher-less SIE file as a no-op, not a failed migration A Fortnox migration aborted with the generic "Något gick fel. Försök igen." when the current fiscal year had nothing booked yet: Fortnox exports an empty SIE file for such a year, the finalizer's 0-entry safety net flipped it to 'failed', and the wizard stopped before the customer/supplier/invoice phase ever ran. Three layered fixes: - finalizeImportRecord only downgrades a 0-entry run to 'failed' when the file actually contained vouchers (parsed count via the documentation object). A file with no vouchers completes as a no-op with an explanatory warning; the mapping-fix retry loop the downgrade exists for (Lookma case) is unchanged. - The migration wizard no longer routes messages that are already user-facing Swedish (server envelopes, ImportResult.errors) through getErrorMessage's Swedish-pattern heuristic, which swallowed unrecognized sentences into the generic fallback. - The heuristic itself learns the import-error family (verifikation/importera) so other surfaces rethrowing engine messages keep the real reason too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(import): cross-check raw #VER before accepting a 0-voucher file as empty The parsed voucher count alone cannot prove a fiscal year was empty: a field-separator or encoding mismatch can swallow every #VER block with only a warning-severity parse issue, and executeSIEImport does not fail on those. Only a raw content check proves the file never declared any vouchers. Addresses the truncation/corruption finding from the Swedish compliance review on #1445. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: record the raw #VER safeguard in the empty-SIE-file decision entry 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> |
||
|
|
2296c0cd59 |
fix(auth): provision invitees server-side when signups are disabled (#1404)
* fix(auth): provision invitees server-side when signups are disabled Self-hosted installations with GoTrue disable_signup broke the invite flow silently: invitees without an account were routed to /register, where supabase.auth.signUp fails with "Signups not allowed for this instance", surfaced only as a generic toast. New server-only env flag AUTH_SIGNUPS_DISABLED (documented in .env.example) mirrors the GoTrue setting. When true, POST /api/company/members/invite checks check_email_exists and, for invitees without an account, provisions one via auth.admin.inviteUserByEmail with a redirect back to /invite/<token>, before the Resend email and before the invitation row is written so a provisioning failure leaves nothing half-created and the admin can retry. The response now carries user_provisioned alongside email_sent, and a provisioning failure returns 502 with a Swedish message mapped through getErrorMessage instead of a silently-successful invite. /auth/callback now routes type=invite verifications to /reset-password (the existing set-password surface) instead of dropping the passwordless user on the dashboard, and preserves the invite token from next=/invite/<token> as the pre-auth invite cookie so the existing reset-password invite handoff accepts the membership right after the password is saved. getErrorMessage learns two GoTrue patterns: "Signups not allowed" (account creation closed on this installation, contact your inviter or administrator) so the /register dead end is explained even for flows that bypass provisioning, and "Error sending ... email" (GoTrue SMTP not configured) so the 502 above is actionable. Hosted is untouched: the flag is unset there and every new code path is gated on it. Fixes #1335 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(auth): restore check_email_exists RPC and harden self-host invite config Adversarial review of #1404 found that the check_email_exists function the invite flow depends on does not exist anywhere: it shipped in PR #229 and was lost in the #244 migration consolidation before ever reaching prod (verified missing on the hosted production database directly). Today app/api/team/accept destructures only { data } from the RPC call, so alreadyHasAccount is silently null on every deployment and the invite page routes even existing-account invitees toward /register. - New migration 20260804140000 restores the function exactly as originally shipped: SECURITY DEFINER over auth.users, EXECUTE revoked from PUBLIC, anon and authenticated, granted to service_role only (prevents email enumeration). Fixes hosted prod behavior too once applied. - New tests/pg/check-email-exists.pg.test.ts locks in existence, case-insensitive matching, false-for-unknown, and the role grants. - .env.docker.example gains the AUTH_SIGNUPS_DISABLED block self-hosters actually use; both env templates now note that the GoTrue redirect URI allow-list must include /invite/* or the invite email redirect silently falls back to SITE_URL. - Invite route test for the existsError branch: RPC failure logs a warning and provisioning proceeds anyway (GoTrue is authoritative). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(auth): mask invitee email in provisioning-failure log (#1335) 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> |
||
|
|
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> |
||
|
|
1270b6daeb |
fix(bookkeeping): let a rättelseverifikation be stornoed; unblock aged supplier-invoice deletion (#1204)
* fix(bookkeeping): let a rättelseverifikation be stornoed; unblock aged supplier-invoice deletion A user who corrected a booking (storno + rättelse) and then discovered the affärshändelse was already booked by another verifikat had no sanctioned way out: reverseEntry refused source_type 'correction' alongside 'storno', and correctEntry rightly rejects a zeroing rättelse (BFL 5 kap 5 §). The same guard also broke uncategorize-after-rättelse, since bank transactions are relinked to the correction entry. - reverseEntry now blocks only 'storno' (storno-of-a-storno keeps the chain ambiguity problem); a correction entry is a regular live verifikat and can be stornoed, with correction_of_id keeping the chain traceable. - CANNOT_REVERSE_STORNO copy narrowed to stornos + remediation hint. - Supplier-invoice DELETE now allows unbooked, unpaid invoices in registered/approved/overdue: the daily overdue cron flipped unbooked invoices past due_date into a state where deletion was blocked forever. Orphan-safety checks (registration JE, payments, accrual schedule) are what actually protect the books. UI shows the delete button accordingly. - LinkVoucherPicker showed customer-side copy (kundfordran/1510) in supplier-invoice mode; supplier mode now explains the 2440-debit requirement, including why a direct-cost verifikat cannot be linked. Support case 2026-07-26 (marcus@). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(supplier-invoices): review fixes: fail-closed orphan lookups, hide delete when payments loaded - The payment and accrual-schedule lookups in DELETE now fail closed: a lookup error returns 500 instead of reading as "nothing linked" and letting the delete proceed unverified. - The delete button also requires the loaded payment list to be empty, matching the server predicate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: authorize 'approved' in supplier-invoice delete allow-list (compliance-swarm V2.3) 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> |
||
|
|
88f53350de |
fix(errors): translate typed engine errors instead of leaking raw messages as journal_entry_error (#1048)
Typed bookkeeping Error instances passed to getErrorMessage() matched the bare-envelope branch (any object with string code + message) and returned their raw English message verbatim, so the categorize and match-invoice routes surfaced strings like DB check-constraint violations directly in the user's toast (issue #337). - get-error-message.ts: when the bare-envelope shape is an Error instance, normalize it into the structured envelope ({ error: { code, message, account_numbers, details } }) so the existing per-code Swedish branches own the translation; plain forwarded envelopes keep the passthrough. - get-error-message.ts: structured-path final fallback now prefers the registry's message_sv for known codes whose message is not Swedish, so typed codes without a dynamic branch (e.g. CANNOT_REVERSE_STORNO) cannot surface English either. - categorize + match-invoice routes: always map the caught error through getErrorMessage (the raw error is already logged); untyped errors fall to the Swedish context fallback instead of leaking err.message. - Tests: new instance-translation suite in lib/errors, typed-error case in the categorize route suite, and deliberate updates of the two tests that pinned raw 'Period locked' passthrough. Fixes #337 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> |
||
|
|
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> |
||
|
|
250cc7c450 |
feat(mcp): always-explicit retryable on structured errors + transient inference (P1-1) (#875)
Agents could not distinguish 'keep retrying' from 'stop, this is
broken' (agent.feedback): retryable was emitted only when a registry
entry declared true — absent otherwise, including for genuinely
transient DB/network failures whose SQLSTATE is lost when tools wrap
them as Error('Database error: ...').
- StructuredError.retryable is now a required boolean. Registry
declaration wins; otherwise isTransientFailure() infers from Postgres
SQLSTATEs (40001/40P01/57014/08xxx/53xxx/55P03), upstream HTTP
statuses (408/429/5xx), and message signatures that survive wrapping
(deadlock, serialization, statement timeout, fetch/socket failures).
- Unclassified transient failures surface as stable code
TRANSIENT_ERROR (new registry entry, retryable: true).
- categorize_transaction accepts idempotency_key — the tool agents
blind-retry after client-side approval-elicitation drops; the key
makes that retry replay-safe instead of double-staging.
- Contract documented in .claude/rules/mcp-server.md. The planned
'kind' field was dropped: the code registry already encodes it;
retryable is the agent-actionable bit.
Every tool error already flows through the single dispatch point
(toToolError -> getStructuredError), so coverage is universal without
per-tool migration. Full unit suite: 6575 tests green.
Part of dev_docs/mcp_optimization_plan.md (P1-1).
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
f9ea9c0082 |
Add/pdf and templates (#705)
* fix(invoices): apply configured voucher series to payments + preview next voucher The booking engine resolves the series from default_voucher_series_per_source_type, but the global "Standardserie" dropdown wrote a separate field the engine ignored, and cash-method invoice payments (invoice_cash_payment) weren't exposed in settings — so configured series were silently dropped to "A". - Expose cash/private payment source types in the per-source-type form - Write the global default through to the map on save, keeping overrides - Resolve voucher-sequences/next by source_type (+date) to match the engine - Show the upcoming voucher (V2) in the payment dialog title - Share resolveInvoicePaymentSourceType so preview and booking can't drift Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(salary): keep AGI panel in sync with Skatteverket signing state The AGI panel mixed run-scoped generation state (agi_generated_at, agi_declarations) with period-scoped submission state (extension_data agi_submission_{period}), so the two could drift and present contradictory UI. Reconcile them: - Auto-detect a Mina Sidor BankID signature: while awaiting_signing, poll /agi/kvittenser on mount and on tab refocus so the panel flips to "signed" (hiding the signing actions) without a manual "Hamta kvittens" click. - Warn instead of offering to sign when the locked granskningsunderlag predates the run's latest AGI generation (draftIsStale) — avoids filing superseded figures. - Self-heal a stale "AGI-XML saknas" error once the run's AGI is (re)generated out-of-band (MCP/API/other tab). - Refetch the salary run on tab focus so agi_generated_at reflects out-of-band generation without a hard reload. - /agi/lasUpp now clears the cached agi_submission_{period} record, so unlocking drops the panel back to the pre-submission state instead of stranding it on a released "redo att signeras" draft. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: Implement VAT registration handling and invoice item line types - Added VAT registration check in commitCreateInvoice to set VAT rate to 0% for non-VAT registered companies. - Updated invoice creation logic to reflect 'exempt' VAT treatment and adjusted related fields accordingly. - Introduced support for free-text and blank spacer rows in invoice items by adding a new line_type field. - Enhanced invoice and credit note handling to accommodate new line types. - Added new localized messages for text rows in English and Swedish. - Created tests for salary run approval logic, ensuring bank details are validated correctly. - Implemented effective net payout calculation for salary runs, considering tax overrides. - Added SQL migrations to support new invoice item line types and accounting method awareness for linking invoices to vouchers. * feat(articles): artikelregister with revenue account + VAT rate per article Article register (non-inventory) with per-article VAT rate and optional BAS class-3 revenue-account override. Includes API routes, UI pages, MCP tools, pending-operation staging, and the activate-or-create account flow (ACCOUNTS_NOT_IN_CHART -> ActivateAccountsDialog, unknown numbers -> AddAccountDialog) reusing the journal entry UX. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(bookkeeping): no-doc-required batch + bulk-missing endpoints Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(payments): supplier payment lines + cash-method invoice matching Shared payment-line proposal for supplier invoices, improved match-invoice/match-supplier-invoice flows (kontantmetoden-aware), and voucher-link support without requiring a 151x clearing entry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(bookkeeping): new journal entry dialog, SIE import tweaks, misc New journal entry dialog component, journal list/page updates, invoice editor updates, SIE import adjustments, transaction ingest and api-key tweaks, pr-agent workflow update. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(invoices): implement tax reduction features and localization updates * feat(tests): add VAT registration gate to pending operations commit tests --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c1be9f15dd |
Post-audit cleanup batch: dead email forks, Docker extension drift, English error locale (#653)
* chore(email): remove dead, diverged email-template forks (audit E2)
extensions/general/email/lib/{invoice,reminder}-templates.ts had zero importers and had diverged from the live lib/email/* copies (which carry later i18n / CSP / Räntelagen-dunning fixes). Pure deletion of a drift hazard.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(docker): hosted preset was missing skatteverket / invoice-inbox / document-extraction / cloud-backup (audit E7)
docker/extensions.hosted.json shipped only 5 of the 9 extensions in extensions.config.json — so a Docker 'hosted' image silently ran without Skatteverket filing, the invoice inbox, document extraction and cloud backup. Aligned with the hosted config.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(errors): return English error messages on the en locale (audit C9)
The structured-error branches in getErrorMessage returned hardcoded Swedish regardless of locale, so English users saw Swedish prose. For the en locale, prefer the registry's English message for any known code; the Swedish (default) path is left entirely unchanged, and codes absent from the registry still fall through. + regression test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
c74b19df1b |
Accounted rebrand + swarm-skill cleanup + bank-reconciliation fixes (#643)
* feat(reconciliation): close the bank-feed loop on voucher links and re-tag mis-typed opening balances
Two related fixes to bank reconciliation correctness:
1. Auto-reconcile on voucher link. Linking an invoice or supplier invoice to
an existing voucher previously advanced only the invoice — the bank
transaction that paid it kept sitting in the Transactions inbox with a null
journal_entry_id. linkInvoiceToVoucher / linkSupplierInvoiceToVoucher now
call autoReconcileTransactionForLinkedVoucher (lib/reconciliation), which
links the bank transaction to the same verifikat when exactly one unbooked
line matches it. Best-effort and post-commit: a failure here never fails the
link. The result surfaces reconciledTransactionId; the inbox row leaves the
list and the UI shows link_success_tx_reconciled.
2. Re-tag mis-typed opening balances. getReconciliationStatus and the GL-line
matching RPCs identify a cash account's ingående balans solely by
journal_entries.source_type='opening_balance'. Companies migrated from other
systems often booked the bank IB as an ordinary voucher (source_type
'import' or 'manual'), so it was never excluded and surfaced as a phantom
reconciliation difference equal to the opening balance. Adds:
- migration mark_entry_as_opening_balance: a GUC-gated carve-out in the
immutability trigger plus a SECURITY DEFINER RPC that validates the entry
(balance-sheet lines only, dated on a fiscal-period boundary), flips the
source_type, and writes an audit row — no blanket data sweep.
- POST /api/reconciliation/bank/mark-opening-balance + MarkOpeningBalanceSchema.
- BankReconciliationView action to trigger it from the IB diff.
The gnubok_create_voucher executor now accepts a typed is_opening_balance flag
and derives source_type='opening_balance' only after validating class 1/2 lines
on the period start, so new IBs land correctly typed.
Covered by lib/reconciliation auto-reconcile tests, voucher-executors tests,
and a mark-entry-as-opening-balance pg-real test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: rebrand gnubok → Accounted and prune swarm agent skills
Product rebrand and skills housekeeping. No runtime behaviour change.
Rebrand: replace user-visible "gnubok" with "Accounted" across docs, READMEs,
in-code comments, doc-site content, MCP skill/resource prose, and the
gnubok-mcp package description. The MCP resource URI scheme is moved gnubok://
→ Accounted:// consistently across resource registrations, the event-type
comment, and the resource/skill tests. Deliberately preserved as stable
identifiers (NOT rebranded): the gnubok-company-id cookie, gnubok_sk_ / gnubok_inv_
token prefixes, the gnubok-mcp npm bridge name, and the AGI <gem:Programnamn>
value (kept 'gnubok' per its source comment — it is the software identifier sent
to Skatteverket and must not churn across visual rebrands).
Skills: remove the 27 swarm-* agent SKILL.md atoms (no longer used; already
absent from the agent_atom_registry in prod), refresh the remaining skill docs,
add the .claude/rules/ path-scoped rule set, and regenerate the
seed_agent_atom_bodies migration + .skill-body-manifest.json via
`npm run skills:generate` so the DB-backed skill bodies match the trimmed set.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
5725c25bf1 |
Logs/improved logging (#398)
* feat(mcp): add create_transactions tool with /pending approval gate New MCP tool gnubok_create_transactions stages 1–10 transactions per call as pending_operations of type create_transaction (risk: medium). Each item becomes its own card on /pending; on confirm, the executor inserts the row into transactions with import_source='mcp' so MCP-staged ingestion is distinguishable from PSD2 sync. Designed for skill workflows that pull external data (e.g., Airtable) and want the user to gate the writes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(bas): strip concatenated group headers from corrupted account names A chart-data import bug had glued the next group's header onto the last account in each preceding group across all eight bas-data class files (e.g. account 2670 read "Utgående moms på försäljning inom EU, OSS 27 PERSONALENS SKATTER, AVGIFTER OCH LÖNEAVDRAG"). The corrupted names surface in transaction dropdowns, ledgers, SIE exports and årsredovisning, and risk VAT miscategorization on the OSS (2670) and blandad-verksamhet (6999) accounts specifically. - Cleans 69 account_name and 64 description fields across class-1..8 files - Adds a regression test asserting no name contains a concatenated header - Ships an idempotent safety-net migration that updates already-seeded chart_of_accounts rows, gated on the corrupted string so user customizations are preserved Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(errors): add structured error codes and handling for various operations - Introduced a new structured error registry in `structured-errors.ts` to standardize error handling across the application. - Added Swedish and English messages for various error scenarios, including validation, authorization, and bookkeeping errors. - Implemented a client-side error toast in `use-error-toast.ts` to display user-friendly error messages with remediation hints. - Created a wrapper for recording operation outcomes in `record-operation.ts`, enhancing audit capabilities for operations. - Developed a provider call wrapper in `with-provider-call.ts` to handle external HTTP calls with structured logging and error mapping. - Added a new SQL migration to extend the processing history with new event types and aggregate types for better operational telemetry. * Refactor supplier API routes to use context-based logging and error handling - Replaced direct Supabase client usage in GET and POST routes with context-based approach using `withRouteContext`. - Enhanced error handling to provide structured error responses for supplier creation and listing. - Updated logging to include request IDs for better traceability. - Introduced new error codes for supplier-related operations. - Refactored tax deadlines cron job to utilize context and improved error handling. - Updated ESLint configuration to enforce logging practices across API and lib directories. - Enhanced arcim migration extension with structured error handling and logging. - Added classification for provider errors to improve user-facing error messages. - Introduced request ID in extension context for better log correlation. * fix(route-context): update DynamicParams type for improved type safety in route handlers * feat(transactions): add 'create_transaction' operation to PendingOperationType * fix(route): ensure companyId is non-nullable in loadAndDeriveAbsence function * fix(route-context): ensure companyId is always non-null by short-circuiting with COMPANY_CONTEXT_MISSING --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
bb855d2ddc |
Add/ai native supp (#385)
* feat(branding): implement dynamic branding in service worker and reports * feat(auth): enhance API key scopes and add bookkeeping write scope - Updated transaction write scope description to include additional tools. - Enhanced reports read scope description to reflect new functionality. - Introduced bookkeeping write scope with relevant description. - Updated SCOPE_GROUPS to include bookkeeping domain. - Modified TOOL_SCOPE_MAP to include new bookkeeping operations. - Updated validateApiKey function to return api_key_id and api_key_name for better actor attribution. feat(tests): add unit tests for MCP resource registry - Created tests for data resources to ensure all required fields are present. - Added tests for resource query parsing and retrieval. feat(resources): implement MCP resources for company and accounting data - Added capabilities resource to expose API key capabilities based on granted scopes. - Implemented chart of accounts resource to retrieve active BAS chart. - Created company current resource to fetch active company details. - Developed active fiscal period resource to check posting eligibility. - Implemented recent activity resource to fetch latest journal entries, invoices, and transactions. - Added VAT treatments resource to provide available VAT rates per customer type. feat(pending-operations): introduce risk tiers for operations - Added risk level classification for pending operations to determine auto-commit eligibility. - Implemented functions to classify operation risk levels and identify high-risk operations. feat(migrations): add actor model and risk tier to pending operations - Updated pending_operations table to include actor type and risk level columns. - Enhanced audit_log to mirror actor information for compliance. - Modified validate_and_increment_api_key function to return actor details. - Expanded operation types in pending_operations to include new high-risk operations. * feat: add auto-commit functionality for low-risk pending operations - Implemented shouldAutoCommit function to determine eligibility for auto-commit based on operation type, actor type, and company settings. - Created commitPendingOperation function to handle execution of pending operations with consistent status updates. - Added tests for shouldAutoCommit to cover various scenarios including high-risk operations, user actors, company opt-in status, and monetary thresholds. - Introduced new columns in company_settings for agent_auto_commit_enabled and agent_auto_commit_max_amount to allow companies to opt-in for auto-commit functionality. - Added SQL migration to update the database schema for new auto-commit settings. * feat(idempotency): implement idempotency key handling for safe retries and cleanup * feat: expand API key scopes and pending operations for bookkeeping - Added 'suppliers:write' scope to API key scopes for supplier invoice management. - Updated SCOPE_GROUPS to include the new 'suppliers:write' scope. - Introduced new pending operation types for bookkeeping: close_period, lock_period, run_year_end, set_opening_balances, run_currency_revaluation, explain_voucher_gap, uncategorize_transaction, approve_supplier_invoice, credit_supplier_invoice, and convert_invoice. - Implemented corresponding commit functions for the new operations in the pending operations module. - Enhanced PendingOperation type to include actor model and risk level attributes. - Added tests for new functionality, ensuring proper behavior and constraints in the database. * feat: implement unlockPeriod functionality and related tests * feat: add agent auto-commit settings and related functionality * feat: add attention resource with comprehensive summary of outstanding tasks * feat: enhance pending operations with 'committing' status and immutability checks, improve idempotency handling, and add original voucher reference for credit notes |
||
|
|
0222e084bb |
Refactor bookkeeping error handling and introduce new error classes (#356)
- Introduced new error classes for better error categorization: - JournalEntryNotBalancedError - FiscalPeriodNotFoundError - EntryDateOutsideFiscalPeriodError - JournalEntryNotFoundError - CannotReverseNonPostedError - CannotCorrectNonPostedError - EntryAlreadyReversedError - CurrencyRevaluationAlreadyExistsError - InvalidMappingResultError - BookkeepingDatabaseError - Updated existing functions in engine.ts and transaction-entries.ts to throw specific errors instead of generic ones. - Enhanced error response handling in get-error-message.ts to provide localized messages for new error types. - Added unit tests for new error classes and error handling functions to ensure correctness and coverage. |