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>
This commit is contained in:
Jakob Wennberg
2026-07-06 15:21:53 +02:00
committed by GitHub
co-authored by Claude Opus 4.7
parent 31b244acf5
commit cdac1808c9
12 changed files with 1311 additions and 148 deletions
@@ -1,16 +1,18 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`v1 spec snapshot > matches the recorded endpoint count > endpoint-count 1`] = `104`;
exports[`v1 spec snapshot > matches the recorded endpoint count > endpoint-count 1`] = `107`;
exports[`v1 spec snapshot > matches the recorded endpoint key set > endpoint-keys 1`] = `
[
"DELETE /api/v1/companies/:companyId/customers/:id",
"DELETE /api/v1/companies/:companyId/dimensions/:id/values/:valueId",
"DELETE /api/v1/companies/:companyId/employees/:id",
"DELETE /api/v1/companies/:companyId/salary-runs/:id",
"DELETE /api/v1/companies/:companyId/suppliers/:id",
"DELETE /api/v1/companies/:companyId/webhooks/:id",
"GET /api/v1/companies",
"GET /api/v1/companies/:companyId/accounts",
"GET /api/v1/companies/:companyId/articles",
"GET /api/v1/companies/:companyId/compliance/check",
"GET /api/v1/companies/:companyId/customers",
"GET /api/v1/companies/:companyId/customers/:id",
@@ -53,6 +55,7 @@ exports[`v1 spec snapshot > matches the recorded endpoint key set > endpoint-key
"GET /api/v1/health",
"GET /api/v1/operations/:id",
"PATCH /api/v1/companies/:companyId/customers/:id",
"PATCH /api/v1/companies/:companyId/dimensions/:id/values/:valueId",
"PATCH /api/v1/companies/:companyId/employees/:id",
"PATCH /api/v1/companies/:companyId/invoices/:id",
"PATCH /api/v1/companies/:companyId/salary-runs/:id",
+18
View File
@@ -0,0 +1,18 @@
/**
* Shared v1 invoice response projections.
*
* The create (POST), detail (GET), and draft-update (PATCH) endpoints all
* return the same invoice shape; keeping the column lists in one module
* prevents response-shape drift between them (a PATCH caller must see the
* same fields a GET caller does). Explicit projection: excludes user_id,
* company_id (internal scoping) and the encrypted personnummer blob
* (deduction_personnummer_last4 is the display-safe representation).
* Schema migrations adding columns must update these lists before the
* field becomes visible on the public API.
*/
export const INVOICE_FULL_COLUMNS =
'id, invoice_number, customer_id, invoice_date, due_date, delivery_date, status, currency, exchange_rate, exchange_rate_date, subtotal, subtotal_sek, vat_amount, vat_amount_sek, total, total_sek, vat_treatment, vat_rate, moms_ruta, your_reference, our_reference, notes, reverse_charge_text, credited_invoice_id, document_type, converted_from_id, paid_at, paid_amount, remaining_amount, default_dimensions, deduction_total, deduction_personnummer_last4, created_at, updated_at'
export const INVOICE_ITEM_FULL_COLUMNS =
'id, sort_order, line_type, description, quantity, unit, unit_price, line_total, vat_rate, vat_amount, article_id, revenue_account, deduction_type, deduction_amount, labor_hours, work_type, housing_designation, apartment_number, brf_org_number, dimensions, created_at'
+5
View File
@@ -135,5 +135,10 @@ import '@/app/api/v1/companies/[companyId]/inbox-items/[id]/stamp/route'
// Dimensions PR2: registry list + value creation (kostnadsställe/projekt).
import '@/app/api/v1/companies/[companyId]/dimensions/route'
import '@/app/api/v1/companies/[companyId]/dimensions/[id]/values/route'
// #895: value lifecycle (rename/archive/end-date + delete-unreferenced).
import '@/app/api/v1/companies/[companyId]/dimensions/[id]/values/[valueId]/route'
// #895: articles read (artikelregister) for invoice line linkage.
import '@/app/api/v1/companies/[companyId]/articles/route'
export {}