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>
61 lines
3.9 KiB
Markdown
61 lines
3.9 KiB
Markdown
---
|
|
paths:
|
|
- "app/api/**"
|
|
---
|
|
|
|
# API Route Pattern
|
|
|
|
Use the `/erp-api-route` skill when scaffolding new endpoints.
|
|
|
|
**Default: wrap every cookie-session route in `withRouteContext`** (`lib/api/with-route-context.ts`). It is the only path that enforces MFA (AAL2) on hosted: it calls `requireAuth()`, resolves the active `companyId`, optionally gates non-viewer role (`requireWrite: true`), and converts thrown errors into the canonical envelope. **Never hand-roll `supabase.auth.getUser()` in a route**: that skips MFA. CI enforces this via the ratchet guard (`npm run check:guards`); a new route calling `getUser()` directly fails the build.
|
|
|
|
```typescript
|
|
import { NextResponse } from 'next/server'
|
|
import { ensureInitialized } from '@/lib/init'
|
|
import { withRouteContext } from '@/lib/api/with-route-context'
|
|
import { validateBody } from '@/lib/api/validate'
|
|
import { MySchema } from '@/lib/api/schemas'
|
|
|
|
ensureInitialized() // Module-level: loads extensions for event emission
|
|
|
|
// Dynamic route: pass the params type as the generic.
|
|
export const POST = withRouteContext<{ params: Promise<{ id: string }> }>(
|
|
'resource.action',
|
|
async (request, { supabase, companyId, user, log }, { params }) => {
|
|
const { id } = await params
|
|
const validation = await validateBody(request, MySchema)
|
|
if (!validation.success) return validation.response
|
|
|
|
// Business logic... always filter by company_id (defense in depth alongside RLS).
|
|
// Throw typed domain errors (e.g. lib/bookkeeping/errors): the wrapper maps
|
|
// them to the right status + canonical { error: { code, message, message_en } }.
|
|
return NextResponse.json({ data: result })
|
|
},
|
|
{ requireWrite: true }, // omit for read-only routes
|
|
)
|
|
```
|
|
|
|
- Dynamic route params: `{ params }: { params: Promise<{ id: string }> }` (Next.js 16, params are async). With `withRouteContext`, pass that shape as the generic and destructure `params` from the 3rd handler arg.
|
|
- Response shapes: `{ data }` for success; failures are the canonical `{ error: { code, message, message_en?, requestId? } }` envelope (thrown errors → `errorResponse`). Don't hand-build `{ error: 'string' }`.
|
|
- Zod schemas in `lib/api/schemas.ts`: 100+ schemas with shared primitives (uuid, isoDate, accountNumber, nonNegativeAmount).
|
|
- Routes that emit events must call `ensureInitialized()` at module level.
|
|
- Opt out of `withRouteContext` only when the route genuinely can't guarantee a company context (e.g. onboarding): then call `requireAuth()` directly so MFA is still enforced.
|
|
- API-key auth (`/api/v1/*`) uses `createServiceClientNoCookies()` + `v1ErrorResponse`; every query still filters by `company_id`.
|
|
|
|
## Endpoint map (`app/api/`)
|
|
|
|
- `/api/bookkeeping/*`: accounts, fiscal periods, journal entries (CRUD/reverse/correct), mapping rules, voucher gaps
|
|
- `/api/invoices/*`, `/api/supplier-invoices/*`: CRUD + state transitions
|
|
- `/api/transactions/*`: categorize, describe, book, match-{invoice,supplier-invoice}, batch, AI suggestions
|
|
- `/api/customers/*`, `/api/suppliers/*`: CRUD
|
|
- `/api/documents/*`: CRUD, versions, link, match-sweep, verify cron
|
|
- `/api/reports/*`: report endpoints (GL, TB, BS, IS, AR/supplier ledger, VAT, SIE, INK2, NE-bilaga, KPI, audit, continuity, monthly, full-archive, salary, vacation, avgifter)
|
|
- `/api/salary/*`: employees, payroll-config, tax-tables, KU, runs
|
|
- `/api/import/*`: bank-file, SIE (parse/execute/mappings)
|
|
- `/api/reconciliation/bank/*`, `/api/settings/*`, `/api/company/*`, `/api/team/*`
|
|
- `/api/deadlines/*`, `/api/tax-deadlines/*`: CRUD + crons
|
|
- `/api/pending-operations/*`, `/api/events/*`, `/api/audit-trail/*`
|
|
- `/api/calendar/feed/[token]`, `/api/mcp-oauth/*`, `/api/support/contact`, `/api/account/delete`
|
|
- `/api/log`, `/api/health`, `/api/vat/validate`, `/api/currency/rate`, `/api/sandbox/*`
|
|
- `/api/extensions/ext/[...path]`: dynamic extension routes (catch-all → `/api/extensions/ext/{extensionId}/{routePath}`, path params as `_paramName` query)
|