New base
This commit is contained in:
@@ -239,6 +239,21 @@ create table extension_data (
|
||||
|
||||
The base never imports from `extensions/`. Dependency flows one direction: extensions import from `lib/core/`, `lib/events/`, `lib/extensions/`.
|
||||
|
||||
### Type ownership
|
||||
|
||||
Extension-specific types live in their extension directory (e.g. `extensions/ne-bilaga/types.ts`). For convenience, `types/index.ts` re-exports them so existing importers continue to work with `import type { NEDeclaration } from '@/types'`. The canonical source is always the extension file.
|
||||
|
||||
| Extension | Type file | Types |
|
||||
|-----------|-----------|-------|
|
||||
| `push-notifications` | `extensions/push-notifications/types.ts` | `PushSubscription`, `NotificationSettings`, `NotificationType`, `NotificationLog` |
|
||||
| `receipt-ocr` | `extensions/receipt-ocr/types.ts` | `Receipt`, `ReceiptLineItem`, `ReceiptExtractionResult`, `ExtractedLineItem`, `ReceiptMatchCandidate`, `ReceiptQueueSummary`, `CameraQualityFeedback`, etc. |
|
||||
| `ne-bilaga` | `extensions/ne-bilaga/types.ts` | `NEDeclaration`, `NEDeclarationRutor`, `NEAccountMapping`, `SRURecord`, `SRUFile`, `NE_RUTA_LABELS` |
|
||||
| `sru-export` | `extensions/sru-export/types.ts` | `SRUExportResult`, `SRUCoverageStats` |
|
||||
|
||||
### API route convention
|
||||
|
||||
All extension API routes live under `app/api/extensions/<extension-name>/`. No extension routes should exist outside this namespace.
|
||||
|
||||
---
|
||||
|
||||
## Add-ons
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
# NE-bilaga Extension
|
||||
|
||||
## Overview
|
||||
|
||||
The NE-bilaga extension generates the NE appendix (Näringsverksamhet) for income tax reporting of enskild firma (sole proprietorship) to Skatteverket. It maps BAS account balances to NE declaration rutor R1-R11 and optionally produces downloadable SRU files.
|
||||
|
||||
Only relevant for **enskild firma** (`entity_type = 'enskild_firma'`). The tab is hidden for AB users.
|
||||
|
||||
Previously embedded as core code in `lib/reports/ne-declaration.ts` and `app/api/reports/ne-declaration/route.ts`, the logic was extracted into a proper extension following the same pattern as `extensions/sru-export/`.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
extensions/ne-bilaga/
|
||||
├── index.ts Extension definition (registered in loader)
|
||||
├── ne-engine.ts Account-to-ruta mapping and balance calculation
|
||||
├── types.ts NE-specific type definitions (canonical source)
|
||||
└── NEDeclarationView.tsx UI component for the NE-bilaga reports tab
|
||||
|
||||
app/api/extensions/ne-bilaga/
|
||||
└── route.ts GET /api/extensions/ne-bilaga (json + sru)
|
||||
```
|
||||
|
||||
### Relationship to SRU Export
|
||||
|
||||
The NE-bilaga extension and the SRU Export extension serve different purposes but share SRU file generation utilities:
|
||||
|
||||
```
|
||||
NE-bilaga path (EF only):
|
||||
ne-engine.ts (hard-coded R1-R11 account mappings) → lib/reports/sru-generator.ts
|
||||
|
||||
SRU Export path (EF + AB):
|
||||
sru-engine.ts (reads sru_code from DB) → sru-generator.ts (any form type)
|
||||
```
|
||||
|
||||
The NE-bilaga engine uses hard-coded account ranges to map balances to NE rutor. The SRU Export engine reads `sru_code` from `chart_of_accounts` for a more generic approach. Both use the shared `sruFileToString()` and `generateSRUFile()` from `lib/reports/sru-generator.ts`.
|
||||
|
||||
### Dead code removed
|
||||
|
||||
The backward-compatibility shim `lib/reports/ne-declaration.ts` (which re-exported from the extension) has been deleted. It had zero importers after the API route `app/api/reports/ne-declaration/route.ts` was removed earlier. All consumers now import directly from the extension or from `@/types`.
|
||||
|
||||
## NE Declaration Rutor
|
||||
|
||||
### Revenue (R1-R4)
|
||||
|
||||
| Ruta | Account Range | Description |
|
||||
|---|---|---|
|
||||
| R1 | 3000-3499 (excl 3100) | Forsaljning med moms (25%) |
|
||||
| R2 | 3100, 3900, 3970-3980 | Momsfria intakter |
|
||||
| R3 | 3200-3299 | Bil/bostadsforman |
|
||||
| R4 | 8310-8330 | Ranteintakter |
|
||||
|
||||
### Expenses (R5-R10)
|
||||
|
||||
| Ruta | Account Range | Description |
|
||||
|---|---|---|
|
||||
| R5 | 4000-4990 | Varuinkop |
|
||||
| R6 | 5000-6990, 7970 | Ovriga kostnader |
|
||||
| R7 | 7000-7699 | Lonekostnader |
|
||||
| R8 | 8400-8499 | Rantekostnader |
|
||||
| R9 | 7820 | Avskrivningar fastighet |
|
||||
| R10 | 7700-7899 (excl 7820) | Avskrivningar ovrigt |
|
||||
|
||||
### Result
|
||||
|
||||
| Ruta | Calculation | Description |
|
||||
|---|---|---|
|
||||
| R11 | (R1+R2+R3+R4) - (R5+R6+R7+R8+R9+R10) | Arets resultat |
|
||||
|
||||
### Gift handling
|
||||
|
||||
- Gifts **with** consideration: R1 (VAT-liable exchange transaction)
|
||||
- Gifts **without** consideration: R2 via account 3900
|
||||
- Deductible gifts: R6 via account 5460
|
||||
|
||||
## API Reference
|
||||
|
||||
### GET /api/extensions/ne-bilaga
|
||||
|
||||
Generate NE declaration for a fiscal period.
|
||||
|
||||
**Query parameters:**
|
||||
|
||||
| Parameter | Required | Description |
|
||||
|---|---|---|
|
||||
| `period_id` | Yes | Fiscal period UUID |
|
||||
| `format` | No | `json` (default) or `sru` |
|
||||
|
||||
**Response (format=json):**
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"fiscalYear": {
|
||||
"id": "uuid",
|
||||
"name": "2025",
|
||||
"start": "2025-01-01",
|
||||
"end": "2025-12-31",
|
||||
"isClosed": false
|
||||
},
|
||||
"rutor": {
|
||||
"R1": 150000,
|
||||
"R2": 0,
|
||||
"R3": 0,
|
||||
"R4": 500,
|
||||
"R5": 45000,
|
||||
"R6": 30000,
|
||||
"R7": 0,
|
||||
"R8": 1200,
|
||||
"R9": 0,
|
||||
"R10": 5000,
|
||||
"R11": 69300
|
||||
},
|
||||
"breakdown": {
|
||||
"R1": {
|
||||
"accounts": [
|
||||
{ "accountNumber": "3001", "accountName": "Forsaljning tjanster 25%", "amount": 150000 }
|
||||
],
|
||||
"total": 150000
|
||||
}
|
||||
},
|
||||
"companyInfo": {
|
||||
"companyName": "Mitt Foretag",
|
||||
"orgNumber": "801234-5678"
|
||||
},
|
||||
"warnings": ["Rakenskapsaret ar inte stangt. Siffrorna kan andras."]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response (format=sru):** Downloads a `.sru` file with `Content-Disposition: attachment`.
|
||||
|
||||
**Error responses:**
|
||||
|
||||
| Status | Condition |
|
||||
|---|---|
|
||||
| 400 | Missing `period_id` parameter |
|
||||
| 401 | Not authenticated |
|
||||
| 500 | Entity type is not `enskild_firma`, period not found, or database error |
|
||||
|
||||
## Engine Details
|
||||
|
||||
### Balance calculation
|
||||
|
||||
1. Fetch all **posted** journal entries for the given fiscal period
|
||||
2. Sum `debit_amount - credit_amount` per account number
|
||||
3. Match each account to an NE ruta using `NE_ACCOUNT_MAPPINGS`
|
||||
4. For revenue accounts (credit-normal): negate the balance so positive = income
|
||||
5. For expense accounts (debit-normal): use as-is
|
||||
6. Round each ruta to whole kronor
|
||||
7. Calculate R11 as total revenue minus total expenses
|
||||
|
||||
### Entity type guard
|
||||
|
||||
The engine throws if `company_settings.entity_type !== 'enskild_firma'`. This prevents accidental NE generation for AB entities.
|
||||
|
||||
### Warnings
|
||||
|
||||
The engine emits warnings when:
|
||||
- The fiscal period is not closed (balances may change)
|
||||
- No revenue or expenses were found (empty period)
|
||||
|
||||
## UI Integration
|
||||
|
||||
### NEDeclarationView component
|
||||
|
||||
The NE-bilaga UI lives in `extensions/ne-bilaga/NEDeclarationView.tsx` and is imported by the reports page:
|
||||
|
||||
```typescript
|
||||
import { NEDeclarationView } from '@/extensions/ne-bilaga/NEDeclarationView'
|
||||
```
|
||||
|
||||
### Reports Page
|
||||
|
||||
The **NE-bilaga** tab in `/reports` is conditionally rendered based on `entity_type`:
|
||||
|
||||
- **Visible** when `entity_type === 'enskild_firma'`
|
||||
- **Hidden** for all other entity types (e.g. `aktiebolag`)
|
||||
|
||||
The entity type is fetched from `GET /api/settings` on page load. The tab shows:
|
||||
|
||||
1. **Info card** with "Hamta NE-bilaga" and "Ladda ner SRU-fil" buttons
|
||||
2. **Warnings card** (orange) if the period is open or no data was found
|
||||
3. **Company info card** with company name, org number, and fiscal year badge
|
||||
4. **Revenue table** (R1-R4) with expandable account-level detail
|
||||
5. **Expenses table** (R5-R10) with expandable account-level detail
|
||||
6. **Result card** (R11) showing net result with green/red color coding
|
||||
|
||||
Each ruta row is clickable to expand and show contributing accounts with individual amounts.
|
||||
|
||||
## Extension Registration
|
||||
|
||||
The extension is registered in `lib/extensions/loader.ts`:
|
||||
|
||||
```typescript
|
||||
import { neBilagaExtension } from '@/extensions/ne-bilaga'
|
||||
|
||||
const FIRST_PARTY_EXTENSIONS: Extension[] = [
|
||||
receiptOcrExtension,
|
||||
aiCategorizationExtension,
|
||||
pushNotificationsExtension,
|
||||
sruExportExtension,
|
||||
neBilagaExtension, // ← added
|
||||
]
|
||||
```
|
||||
|
||||
The extension declares a single report type (`ne-bilaga`) for discovery via `extensionRegistry.getByCapability('reportTypes')`. It has no event handlers, no settings panel, and no sidebar items.
|
||||
|
||||
## Type Definitions
|
||||
|
||||
Canonical source: `extensions/ne-bilaga/types.ts` (re-exported from `types/index.ts` for convenience):
|
||||
|
||||
- **`NEDeclaration`** — Top-level response shape with fiscal year, rutor, breakdown, company info, warnings
|
||||
- **`NEDeclarationRutor`** — Record of R1-R11 number values
|
||||
- **`NEAccountMapping`** — Mapping config: ruta, account ranges (with exclusions), isExpense flag
|
||||
- **`NE_RUTA_LABELS`** — Display labels for each ruta
|
||||
- **`SRURecord`** — Single SRU field code + value pair
|
||||
- **`SRUFile`** — Collection of SRU records with generation timestamp
|
||||
|
||||
## Files Changed
|
||||
|
||||
### Created
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `extensions/ne-bilaga/index.ts` | Extension definition with report type registration |
|
||||
| `extensions/ne-bilaga/ne-engine.ts` | Account mapping + balance calculation (moved from `lib/reports/`) |
|
||||
| `extensions/ne-bilaga/types.ts` | Canonical type definitions (`NEDeclaration`, `NEDeclarationRutor`, etc.) |
|
||||
| `extensions/ne-bilaga/NEDeclarationView.tsx` | UI component (extracted from reports page) |
|
||||
| `app/api/extensions/ne-bilaga/route.ts` | API endpoint with json/sru format support |
|
||||
|
||||
### Modified
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `lib/extensions/loader.ts` | Added `neBilagaExtension` to `FIRST_PARTY_EXTENSIONS` |
|
||||
| `app/(dashboard)/reports/page.tsx` | Imports `NEDeclarationView` from extension instead of inlining it |
|
||||
| `types/index.ts` | NE types replaced with re-exports from `extensions/ne-bilaga/types.ts` |
|
||||
|
||||
### Deleted
|
||||
|
||||
| File | Reason |
|
||||
|---|---|
|
||||
| `app/api/reports/ne-declaration/route.ts` | Replaced by `app/api/extensions/ne-bilaga/route.ts` |
|
||||
| `lib/reports/ne-declaration.ts` | Dead backward-compat shim with zero importers |
|
||||
|
||||
### Reused (not modified)
|
||||
|
||||
| File | What was reused |
|
||||
|---|---|
|
||||
| `lib/reports/sru-generator.ts` | `generateSRUFile()`, `sruFileToString()`, `getSRUFilename()` |
|
||||
|
||||
## Verification
|
||||
|
||||
- `npx tsc --noEmit` — zero errors
|
||||
- `GET /api/extensions/ne-bilaga?period_id=X&format=json` returns NE declaration data
|
||||
- `GET /api/extensions/ne-bilaga?period_id=X&format=sru` downloads `.sru` file
|
||||
- NE-bilaga tab visible for EF users, hidden for AB users
|
||||
- Reports page renders identically to before for EF users
|
||||
- Extension appears in `extensionRegistry.getAll()` and `extensionRegistry.getByCapability('reportTypes')`
|
||||
- `next build` succeeds with zero errors
|
||||
@@ -0,0 +1,312 @@
|
||||
# Push Notifications Extension
|
||||
|
||||
## Overview
|
||||
|
||||
The `push-notifications` extension converts system events into instant push notifications delivered via the Web Push API. It is the third first-party extension, following `receipt-ocr` and `ai-categorization`.
|
||||
|
||||
The extension operates in two modes:
|
||||
|
||||
1. **Event-driven** — Instant notifications triggered by the event bus (`period.locked`, `receipt.matched`, etc.)
|
||||
2. **Cron-based** — Scheduled checks for time-dependent conditions (upcoming tax deadlines, overdue invoices)
|
||||
|
||||
Both modes share a single send pipeline that handles settings checks, quiet hours, duplicate prevention, delivery, logging, and subscription cleanup.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Event Bus
|
||||
|
|
||||
+---------+---------+
|
||||
| |
|
||||
period.locked receipt.matched
|
||||
invoice.sent receipt.extracted
|
||||
period.year_closed
|
||||
| |
|
||||
v v
|
||||
+-----------------------------+
|
||||
| Event Handlers (index.ts) |
|
||||
| gate: check setting |
|
||||
| build: payload-builders |
|
||||
+-------------+---------------+
|
||||
|
|
||||
v
|
||||
+-----------------------------+ +---------------------------+
|
||||
| sendNotificationToUser() | <---- | Cron Scheduler |
|
||||
| (notification-sender.ts) | | (notification-scheduler) |
|
||||
| | | tax deadlines, invoices |
|
||||
| 1. push_enabled? | +---------------------------+
|
||||
| 2. quiet hours? |
|
||||
| 3. duplicate? |
|
||||
| 4. get subscriptions |
|
||||
| 5. web-push send |
|
||||
| 6. log to notification_log |
|
||||
| 7. disable 410 subs |
|
||||
+-----------------------------+
|
||||
```
|
||||
|
||||
### File Structure
|
||||
|
||||
```
|
||||
extensions/push-notifications/
|
||||
index.ts # Extension object, settings, 5 event handlers
|
||||
notification-sender.ts # Unified send pipeline, VAPID config, helpers
|
||||
payload-builders.ts # All notification payload constructors
|
||||
notification-scheduler.ts # Cron-based tax deadline & invoice scheduling
|
||||
|
||||
app/api/extensions/push-notifications/
|
||||
settings/route.ts # GET/PATCH settings API
|
||||
cron/route.ts # Daily cron (imports from extension)
|
||||
subscribe/route.ts # Subscription management (imports VAPID from extension)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Event Subscriptions
|
||||
|
||||
| Event | Handler | Default Enabled | Notification Content |
|
||||
|-------|---------|-----------------|---------------------|
|
||||
| `period.locked` | `handlePeriodLocked` | Yes | "{period.name} har lasts" |
|
||||
| `period.year_closed` | `handleYearClosed` | Yes | "Arsbokslut klart for {period.name}" |
|
||||
| `invoice.sent` | `handleInvoiceSent` | No | "Faktura {number} skickad" |
|
||||
| `receipt.extracted` | `handleReceiptExtracted` | Yes | "Kvitto analyserat: {merchant}" |
|
||||
| `receipt.matched` | `handleReceiptMatched` | Yes | "Kvitto matchat mot transaktion" |
|
||||
|
||||
Each event handler follows the **gate pattern**:
|
||||
1. Extract `userId` from event payload
|
||||
2. Load extension settings from `extension_data`
|
||||
3. Check if the specific notification type is enabled
|
||||
4. Build the payload via `payload-builders.ts`
|
||||
5. Call `sendNotificationToUser()` from the sender module
|
||||
|
||||
---
|
||||
|
||||
## Cron Scheduling
|
||||
|
||||
The cron endpoint (`GET /api/extensions/push-notifications/cron`) runs daily at 09:00 via Vercel Cron and handles two time-dependent notification types:
|
||||
|
||||
### Tax Deadlines
|
||||
Queries the `deadlines` table for uncompleted tax deadlines due in 7 days, 1 day, or today. Checks the user-level `tax_deadlines_enabled` setting in `notification_settings` before sending.
|
||||
|
||||
### Invoice Reminders
|
||||
Queries the `invoices` table for sent/overdue invoices due in 3 days, today, or overdue by 3/7 days. Checks the user-level `invoice_reminders_enabled` setting before sending.
|
||||
|
||||
Both call `sendNotificationToUser()` which handles the full pipeline (quiet hours, duplicate check, send, log).
|
||||
|
||||
---
|
||||
|
||||
## Settings
|
||||
|
||||
### Extension Settings (event-driven toggles)
|
||||
|
||||
Stored in the `extension_data` table under `extension_id = 'push-notifications'`, `key = 'settings'`.
|
||||
|
||||
```typescript
|
||||
interface PushNotificationSettings {
|
||||
periodLockedEnabled: boolean // default: true
|
||||
periodYearClosedEnabled: boolean // default: true
|
||||
invoiceSentEnabled: boolean // default: false
|
||||
receiptExtractedEnabled: boolean // default: true
|
||||
receiptMatchedEnabled: boolean // default: true
|
||||
}
|
||||
```
|
||||
|
||||
### First-Party Settings (existing tables)
|
||||
|
||||
The `notification_settings` table controls transport-level and category-level preferences:
|
||||
|
||||
| Column | Type | Purpose |
|
||||
|--------|------|---------|
|
||||
| `push_enabled` | boolean | Master push toggle |
|
||||
| `tax_deadlines_enabled` | boolean | Cron: tax deadline notifications |
|
||||
| `invoice_reminders_enabled` | boolean | Cron: invoice due/overdue |
|
||||
| `quiet_start` | text | Quiet hours start (e.g., "21:00") |
|
||||
| `quiet_end` | text | Quiet hours end (e.g., "08:00") |
|
||||
|
||||
Both layers are checked before sending. Extension settings gate event-driven notifications; `notification_settings` gates the transport and cron-based categories.
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
### GET /api/extensions/push-notifications/settings
|
||||
|
||||
Returns the current user's event-driven notification toggles.
|
||||
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"periodLockedEnabled": true,
|
||||
"periodYearClosedEnabled": true,
|
||||
"invoiceSentEnabled": false,
|
||||
"receiptExtractedEnabled": true,
|
||||
"receiptMatchedEnabled": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response 401:** `{ "error": "Unauthorized" }`
|
||||
|
||||
### PATCH /api/extensions/push-notifications/settings
|
||||
|
||||
Updates one or more event-driven notification toggles. Only the provided keys are updated; others remain unchanged.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"receiptExtractedEnabled": false,
|
||||
"invoiceSentEnabled": true
|
||||
}
|
||||
```
|
||||
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"periodLockedEnabled": true,
|
||||
"periodYearClosedEnabled": true,
|
||||
"invoiceSentEnabled": true,
|
||||
"receiptExtractedEnabled": false,
|
||||
"receiptMatchedEnabled": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response 400:** `{ "error": "No valid settings provided" }`
|
||||
|
||||
**Response 401:** `{ "error": "Unauthorized" }`
|
||||
|
||||
**Allowed keys:** `periodLockedEnabled`, `periodYearClosedEnabled`, `invoiceSentEnabled`, `receiptExtractedEnabled`, `receiptMatchedEnabled`
|
||||
|
||||
---
|
||||
|
||||
## Notification Types
|
||||
|
||||
The `NotificationType` union is defined in `extensions/push-notifications/types.ts` (re-exported from `types/index.ts` for convenience):
|
||||
|
||||
| Type | Source | Description |
|
||||
|------|--------|-------------|
|
||||
| `tax_deadline` | Cron | Upcoming tax deadline |
|
||||
| `invoice_due` | Cron | Invoice approaching due date |
|
||||
| `invoice_overdue` | Cron | Invoice past due date |
|
||||
| `period_locked` | Event | Fiscal period was locked |
|
||||
| `period_year_closed` | Event | Year-end closing completed |
|
||||
| `receipt_extracted` | Event | Receipt OCR completed |
|
||||
| `receipt_matched` | Event | Receipt matched to transaction |
|
||||
| `invoice_sent` | Event | Invoice was sent |
|
||||
|
||||
These are stored as text in `notification_log.notification_type` (no migration needed).
|
||||
|
||||
---
|
||||
|
||||
## Database Tables Used
|
||||
|
||||
No new tables or migrations were required. The extension uses existing tables:
|
||||
|
||||
| Table | Usage |
|
||||
|-------|-------|
|
||||
| `push_subscriptions` | User's active Web Push subscriptions |
|
||||
| `notification_settings` | Transport-level and category-level preferences |
|
||||
| `notification_log` | Sent notification history (duplicate prevention) |
|
||||
| `extension_data` | Event-driven toggle settings |
|
||||
| `deadlines` | Tax deadline queries (cron) |
|
||||
| `invoices` | Invoice due/overdue queries (cron) |
|
||||
|
||||
---
|
||||
|
||||
## Send Pipeline
|
||||
|
||||
`sendNotificationToUser(supabase, userId, payload, notificationType, referenceId, daysBefore?)` executes the following steps:
|
||||
|
||||
1. **Settings check** — Load `notification_settings` for the user. If `push_enabled` is false, skip.
|
||||
2. **Quiet hours** — Convert current time to Sweden timezone (`Europe/Stockholm`). If within the user's quiet hours window, skip.
|
||||
3. **Duplicate check** — Query `notification_log` for a matching `(user_id, notification_type, reference_id, days_before)` tuple. If found, skip.
|
||||
4. **Get subscriptions** — Fetch all active push subscriptions from `push_subscriptions`. If none, skip.
|
||||
5. **Send** — Deliver via the `web-push` library to all subscriptions using `Promise.allSettled`.
|
||||
6. **Log** — Insert a record into `notification_log` with `delivery_status: 'sent'`.
|
||||
7. **Cleanup** — Any subscription returning HTTP 410 (Gone) is marked `is_active: false`.
|
||||
|
||||
Returns `{ sent: boolean, reason?: string }` where reason can be: `push_disabled`, `quiet_hours`, `duplicate`, `no_subscriptions`, `send_failed`, or `error`.
|
||||
|
||||
---
|
||||
|
||||
## Registration
|
||||
|
||||
The extension is registered in `lib/extensions/loader.ts` alongside the other first-party extensions:
|
||||
|
||||
```typescript
|
||||
const FIRST_PARTY_EXTENSIONS: Extension[] = [
|
||||
receiptOcrExtension,
|
||||
aiCategorizationExtension,
|
||||
pushNotificationsExtension,
|
||||
]
|
||||
```
|
||||
|
||||
When `loadExtensions()` is called, the registry wires up all 5 event handlers to the event bus.
|
||||
|
||||
---
|
||||
|
||||
## Refactoring Notes
|
||||
|
||||
### Phase 1: Logic extraction (original)
|
||||
|
||||
The following files were deleted. All logic was moved into the extension:
|
||||
|
||||
| Deleted File | Logic Moved To |
|
||||
|-------------|----------------|
|
||||
| `lib/push/web-push.ts` | `extensions/push-notifications/notification-sender.ts` (VAPID config, send functions, types) + `extensions/push-notifications/payload-builders.ts` (payload constructors) |
|
||||
| `lib/push/notification-scheduler.ts` | `extensions/push-notifications/notification-scheduler.ts` (cron scheduling) + `extensions/push-notifications/notification-sender.ts` (quiet hours, duplicate check, logging, subscription management) |
|
||||
|
||||
### Phase 2: Route relocation
|
||||
|
||||
The API routes were moved from `app/api/push/` into the extension namespace to enforce the architecture rule that the core never depends on extension code:
|
||||
|
||||
| Old Path | New Path |
|
||||
|----------|----------|
|
||||
| `app/api/push/cron/route.ts` | `app/api/extensions/push-notifications/cron/route.ts` |
|
||||
| `app/api/push/subscribe/route.ts` | `app/api/extensions/push-notifications/subscribe/route.ts` |
|
||||
|
||||
Updated consumers:
|
||||
- `components/push/PushPrompt.tsx` — fetch URLs updated to `/api/extensions/push-notifications/subscribe`
|
||||
- `components/settings/NotificationSettings.tsx` — fetch URLs updated to `/api/extensions/push-notifications/subscribe`
|
||||
- `vercel.json` — cron path updated to `/api/extensions/push-notifications/cron`
|
||||
|
||||
### Phase 3: Type extraction
|
||||
|
||||
Push notification types (`PushSubscription`, `NotificationSettings`, `NotificationType`, `NotificationLog`) were moved from `types/index.ts` to `extensions/push-notifications/types.ts`. Re-exports in `types/index.ts` preserve backward compatibility.
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables
|
||||
|
||||
The extension requires the following environment variables (unchanged from before):
|
||||
|
||||
| Variable | Purpose |
|
||||
|----------|---------|
|
||||
| `NEXT_PUBLIC_VAPID_PUBLIC_KEY` | VAPID public key for client-side subscription |
|
||||
| `VAPID_PRIVATE_KEY` | VAPID private key for server-side sending |
|
||||
| `VAPID_SUBJECT` | VAPID subject (defaults to `mailto:support@erp-base.se`) |
|
||||
| `CRON_SECRET` | Secret for authenticating cron requests |
|
||||
| `NEXT_PUBLIC_SUPABASE_URL` | Supabase project URL (used by cron service client) |
|
||||
| `SUPABASE_SERVICE_ROLE_KEY` | Supabase service role key (used by cron service client) |
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
### Automated
|
||||
- `npx tsc --noEmit` — Zero TypeScript errors
|
||||
- `npx vitest run` — All 78 existing tests pass
|
||||
|
||||
### Manual Verification
|
||||
|
||||
1. **Period locked** — Lock a period via the UI. If the user has an active push subscription and `periodLockedEnabled` is true, they receive a push notification.
|
||||
2. **Receipt extracted** — Upload a receipt image. After OCR completes, the `receipt.extracted` event fires and triggers a push notification.
|
||||
3. **Receipt matched** — When a receipt is auto-matched to a transaction, the `receipt.matched` event triggers a push notification.
|
||||
4. **Invoice sent** — Send an invoice. If `invoiceSentEnabled` is true (default: false), a push notification is sent.
|
||||
5. **Year closed** — Complete a year-end closing. The `period.year_closed` event triggers a push notification.
|
||||
6. **Settings API** — `GET /api/extensions/push-notifications/settings` returns default settings. `PATCH` with `{ "receiptExtractedEnabled": false }` disables that specific notification type.
|
||||
7. **Cron** — `GET /api/extensions/push-notifications/cron` (with Bearer token) runs the tax deadline and invoice reminder checks using the extension's scheduler module.
|
||||
8. **Disable toggle** — Set `receiptExtractedEnabled: false` via PATCH, then upload a receipt. No push notification should be sent for the extraction event.
|
||||
@@ -0,0 +1,257 @@
|
||||
# SRU Export Extension
|
||||
|
||||
## Overview
|
||||
|
||||
The SRU Export extension generates SRU (Standardiserat Räkenskapsutdrag) files for electronic tax filing with Skatteverket. It reads `sru_code` from `chart_of_accounts`, aggregates balances by SRU code, and produces downloadable `.sru` files.
|
||||
|
||||
Supports both:
|
||||
- **NE** (Enskild firma) — field codes 7310-7350
|
||||
- **INK2** (Aktiebolag) — field codes 7200-7499
|
||||
|
||||
The form type is determined automatically from the user's `entity_type` in `company_settings`.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
extensions/sru-export/
|
||||
├── index.ts Extension definition (registered in loader)
|
||||
├── sru-engine.ts Balance aggregation by SRU code
|
||||
├── sru-generator.ts Generic SRU file generation
|
||||
├── types.ts SRU-specific type definitions (canonical source)
|
||||
└── SRUExportView.tsx UI component for the SRU-export reports tab
|
||||
|
||||
app/api/extensions/sru-export/
|
||||
├── route.ts GET /api/extensions/sru-export (export endpoint)
|
||||
└── coverage/
|
||||
└── route.ts GET /api/extensions/sru-export/coverage (stats)
|
||||
```
|
||||
|
||||
### Relationship to NE-bilaga extension
|
||||
|
||||
The NE-bilaga extension (`extensions/ne-bilaga/`) handles the NE-specific declaration path with hard-coded R1-R11 account mappings, served via `/api/extensions/ne-bilaga`. See `dev_docs/base_architecture/NE-BILAGA-EXTENSION.md`.
|
||||
|
||||
The generic SRU export is an alternative path that works from raw `account → sru_code` mappings stored in `chart_of_accounts`, rather than hard-coded NE ruta mappings. The generic generator reuses `sruFileToString()` and `validateSRUFile()` from the shared module.
|
||||
|
||||
```
|
||||
NE-bilaga path (EF only):
|
||||
extensions/ne-bilaga/ne-engine.ts → lib/reports/sru-generator.ts (NE field codes)
|
||||
|
||||
Generic path (NE + INK2):
|
||||
extensions/sru-export/sru-engine.ts (reads sru_code from DB) → sru-generator.ts (any form type)
|
||||
```
|
||||
|
||||
## Database Changes
|
||||
|
||||
### Migration: `20240101000021_sru_codes.sql`
|
||||
|
||||
Applied as three remote migrations:
|
||||
1. `add_sru_code_column` — `ALTER TABLE chart_of_accounts ADD COLUMN sru_code text`
|
||||
2. `sru_codes_backfill` — Updates existing accounts with SRU codes based on account number ranges
|
||||
3. `sru_codes_seed_function` — Updates `seed_chart_of_accounts()` to include `sru_code` for new users
|
||||
|
||||
The backfill only updates accounts where `sru_code IS NULL`, preserving any manual assignments.
|
||||
|
||||
### SRU Code Mappings
|
||||
|
||||
#### NE form (EF) — Revenue & Expense accounts
|
||||
|
||||
| Account Range | SRU Code | NE Ruta | Description |
|
||||
|---|---|---|---|
|
||||
| 3000-3499 (excl 3100) | 7310 | R1 | Försäljning med moms |
|
||||
| 3100, 3900, 3970-3980 | 7311 | R2 | Momsfria intäkter |
|
||||
| 3200-3299 | 7312 | R3 | Bil/bostadsförmån |
|
||||
| 8310-8330 | 7313 | R4 | Ränteintäkter |
|
||||
| 4000-4990 | 7320 | R5 | Varuinköp |
|
||||
| 5000-6990, 7970 | 7321 | R6 | Övriga kostnader |
|
||||
| 7000-7699 | 7322 | R7 | Lönekostnader |
|
||||
| 8400-8499 | 7323 | R8 | Räntekostnader |
|
||||
| 7820 | 7324 | R9 | Avskrivningar fastighet |
|
||||
| 7700-7899 (excl 7820) | 7325 | R10 | Avskrivningar övrigt |
|
||||
|
||||
#### INK2 form (AB) — Balance Sheet accounts
|
||||
|
||||
| Account Range | SRU Code | Description |
|
||||
|---|---|---|
|
||||
| 1000-1099 | 7201 | Immateriella anläggningstillgångar |
|
||||
| 1100-1299 | 7202 | Materiella anläggningstillgångar |
|
||||
| 1300-1399 | 7203 | Finansiella anläggningstillgångar |
|
||||
| 1400-1499 | 7210 | Varulager |
|
||||
| 1500-1599 | 7211 | Kundfordringar |
|
||||
| 1600-1999 | 7212 | Övriga omsättningstillgångar |
|
||||
| 2081 | 7220 | Aktiekapital |
|
||||
| 2085-2098 | 7221 | Övrigt eget kapital |
|
||||
| 2099 | 7222 | Årets resultat |
|
||||
| 2100-2499 | 7230 | Skulder |
|
||||
| 2500-2999 | 7231 | Övriga skulder |
|
||||
|
||||
#### INK2 form (AB) — Income Statement accounts
|
||||
|
||||
| Account Range | SRU Code | Description |
|
||||
|---|---|---|
|
||||
| 3000-3999 | 7310 | Nettoomsättning |
|
||||
| 4000-4999 | 7320 | Varuinköp |
|
||||
| 5000-6999 | 7330 | Övriga externa kostnader |
|
||||
| 7000-7699 | 7340 | Personalkostnader |
|
||||
| 7700-7899 | 7350 | Avskrivningar |
|
||||
| 7900-7999 | 7360 | Övriga rörelsekostnader |
|
||||
| 8000-8499 | 7370 | Finansiella poster |
|
||||
| 8500-8999 | 7380 | Extraordinära poster |
|
||||
|
||||
## API Reference
|
||||
|
||||
### GET /api/extensions/sru-export
|
||||
|
||||
Generate SRU export for a fiscal period.
|
||||
|
||||
**Query parameters:**
|
||||
| Parameter | Required | Description |
|
||||
|---|---|---|
|
||||
| `period_id` | Yes | Fiscal period UUID |
|
||||
| `format` | No | `json` (default) or `sru` |
|
||||
|
||||
**Response (format=json):**
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"formType": "NE",
|
||||
"entityType": "enskild_firma",
|
||||
"companyName": "Mitt Företag AB",
|
||||
"orgNumber": "556123-4567",
|
||||
"fiscalYear": {
|
||||
"id": "uuid",
|
||||
"name": "2025",
|
||||
"start": "2025-01-01",
|
||||
"end": "2025-12-31"
|
||||
},
|
||||
"balances": [
|
||||
{
|
||||
"sruCode": "7310",
|
||||
"description": "Försäljning med moms",
|
||||
"amount": 150000,
|
||||
"accounts": [
|
||||
{ "accountNumber": "3001", "accountName": "Försäljning tjänster 25%", "amount": 150000 }
|
||||
]
|
||||
}
|
||||
],
|
||||
"warnings": ["Räkenskapsåret är inte stängt. Siffrorna kan ändras."]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response (format=sru):** Downloads a `.sru` file with `Content-Disposition: attachment`.
|
||||
|
||||
### GET /api/extensions/sru-export/coverage
|
||||
|
||||
Returns SRU code coverage statistics for the authenticated user's chart of accounts.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"totalAccounts": 30,
|
||||
"accountsWithSRU": 28,
|
||||
"accountsWithoutSRU": 2,
|
||||
"coveragePercent": 93,
|
||||
"missingAccounts": [
|
||||
{ "accountNumber": "1220", "accountName": "Inventarier" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## SRU File Format
|
||||
|
||||
The generated `.sru` file follows the Skatteverket standard:
|
||||
|
||||
```
|
||||
#PRODUKT KONTROLLUPPGIFTER
|
||||
#SESSION 1
|
||||
#PROGRAMNAMN ERPBase
|
||||
#PROGRAMVERSION 1.0
|
||||
#SKAPAT 20260219
|
||||
#BLANKETT NE
|
||||
#IDENTITET 5561234567
|
||||
#UPPGIFT 7000 20250101-20251231
|
||||
#UPPGIFT 7310 150000
|
||||
#UPPGIFT 7320 -45000
|
||||
#UPPGIFT 7321 -30000
|
||||
#BLANKETTSLUT
|
||||
```
|
||||
|
||||
Each `#UPPGIFT` line contains an SRU field code and the rounded (whole kronor) amount. Zero-amount entries are omitted.
|
||||
|
||||
## UI Integration
|
||||
|
||||
### SRUExportView component
|
||||
|
||||
The SRU export UI lives in `extensions/sru-export/SRUExportView.tsx` and is imported by the reports page:
|
||||
|
||||
```typescript
|
||||
import { SRUExportView } from '@/extensions/sru-export/SRUExportView'
|
||||
```
|
||||
|
||||
### Reports Page
|
||||
|
||||
The **SRU-export** tab in `/reports` shows:
|
||||
|
||||
1. **Info card** with "Förhandsgranska" and "Ladda ner SRU-fil" buttons
|
||||
2. **Coverage warning** if accounts lack SRU codes (fetched from `/coverage` endpoint)
|
||||
3. **Company info card** showing entity type badge (NE/INK2) and fiscal year
|
||||
4. **SRU balances table** with expandable rows showing per-account detail
|
||||
|
||||
### Chart of Accounts
|
||||
|
||||
A new **SRU** column is added to the accounts table in the Kontoplan view. The column is inline-editable: click a cell to type a new SRU code, press Enter to save, Escape to cancel. Updates go directly to `chart_of_accounts.sru_code` via the Supabase client.
|
||||
|
||||
## Extension Registration
|
||||
|
||||
The extension is registered in `lib/extensions/loader.ts`:
|
||||
|
||||
```typescript
|
||||
import { sruExportExtension } from '@/extensions/sru-export'
|
||||
|
||||
const FIRST_PARTY_EXTENSIONS: Extension[] = [
|
||||
receiptOcrExtension,
|
||||
aiCategorizationExtension,
|
||||
pushNotificationsExtension,
|
||||
sruExportExtension, // ← added
|
||||
]
|
||||
```
|
||||
|
||||
The extension has no event handlers, no settings panel, and no sidebar items. It only declares a report type for discovery purposes.
|
||||
|
||||
## Type Definitions
|
||||
|
||||
Canonical source: `extensions/sru-export/types.ts` (re-exported from `types/index.ts` for convenience):
|
||||
|
||||
- **`SRUExportResult`** — Response shape for the JSON format export endpoint
|
||||
- **`SRUCoverageStats`** — Response shape for the coverage endpoint
|
||||
|
||||
## Files Changed
|
||||
|
||||
### Created
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `supabase/migrations/20240101000021_sru_codes.sql` | Add column, backfill SRU codes, update seed function |
|
||||
| `extensions/sru-export/index.ts` | Extension definition |
|
||||
| `extensions/sru-export/sru-engine.ts` | Balance aggregation by SRU code |
|
||||
| `extensions/sru-export/sru-generator.ts` | Generic SRU file generation |
|
||||
| `extensions/sru-export/types.ts` | Canonical type definitions (`SRUExportResult`, `SRUCoverageStats`) |
|
||||
| `extensions/sru-export/SRUExportView.tsx` | UI component (extracted from reports page) |
|
||||
| `app/api/extensions/sru-export/route.ts` | Export API endpoint |
|
||||
| `app/api/extensions/sru-export/coverage/route.ts` | Coverage stats endpoint |
|
||||
|
||||
### Modified
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `lib/extensions/loader.ts` | Added `sruExportExtension` to `FIRST_PARTY_EXTENSIONS` |
|
||||
| `app/(dashboard)/reports/page.tsx` | Imports `SRUExportView` from extension instead of inlining it |
|
||||
| `types/index.ts` | SRU types replaced with re-exports from `extensions/sru-export/types.ts` |
|
||||
| `components/bookkeeping/ChartOfAccounts.tsx` | Added inline-editable SRU code column |
|
||||
|
||||
### Reused (not modified)
|
||||
| File | What was reused |
|
||||
|---|---|
|
||||
| `lib/reports/sru-generator.ts` | `sruFileToString()`, `validateSRUFile()` |
|
||||
| `extensions/ne-bilaga/ne-engine.ts` | Balance calculation pattern |
|
||||
| `lib/reports/sie-export.ts` | `calculateBalances()` pattern |
|
||||
Reference in New Issue
Block a user