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>
This commit is contained in:
Jakob Wennberg
2026-08-04 19:46:13 +02:00
committed by GitHub
co-authored by Claude Fable 5 Jakob Wennberg
parent 9f5a43310b
commit 86c6af6976
7 changed files with 796 additions and 2 deletions
@@ -1,6 +1,6 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`v1 spec snapshot > matches the recorded endpoint count > endpoint-count 1`] = `123`;
exports[`v1 spec snapshot > matches the recorded endpoint count > endpoint-count 1`] = `124`;
exports[`v1 spec snapshot > matches the recorded endpoint key set > endpoint-keys 1`] = `
[
@@ -69,6 +69,7 @@ exports[`v1 spec snapshot > matches the recorded endpoint key set > endpoint-key
"PATCH /api/v1/companies/:companyId/invoices/:id",
"PATCH /api/v1/companies/:companyId/salary-runs/:id",
"PATCH /api/v1/companies/:companyId/salary-runs/:id/lines/:lineId",
"PATCH /api/v1/companies/:companyId/settings",
"PATCH /api/v1/companies/:companyId/supplier-invoices/:id",
"PATCH /api/v1/companies/:companyId/suppliers/:id",
"PATCH /api/v1/companies/:companyId/webhooks/:id",
@@ -134,6 +135,7 @@ exports[`v1 spec snapshot > matches the recorded scope catalogue > endpoint-scop
[
"bookkeeping:write",
"companies:read",
"companies:write",
"compliance:read",
"customers:read",
"customers:write",
+3
View File
@@ -161,4 +161,7 @@ import '@/app/api/v1/companies/[companyId]/dimensions/[id]/values/[valueId]/rout
// #895: articles read (artikelregister) for invoice line linkage.
import '@/app/api/v1/companies/[companyId]/articles/route'
// #1348: company-settings write (PATCH, MCP-tool-identical field set).
import '@/app/api/v1/companies/[companyId]/settings/route'
export {}