Files
accounted/dev_docs/04-API-SPECIFICATION.md
T
2026-02-13 14:00:46 +01:00

673 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# API Specification
## Authentication
All API routes require authentication via Supabase session cookie.
```typescript
// lib/supabase/server.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export async function createClient() {
const cookieStore = cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return cookieStore.get(name)?.value
},
},
}
)
}
// Usage in route
export async function GET() {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
return Response.json({ error: 'Unauthorized' }, { status: 401 })
}
// ... route logic
}
```
---
## Endpoints
### Company Settings
#### GET /api/settings
Returns current user's company settings.
**Response 200:**
```json
{
"id": "uuid",
"company_name": "Anna Andersson Content AB",
"org_number": "123456-7890",
"vat_number": "SE1234567890",
"vat_registered": true,
"address_line1": "Storgatan 1",
"city": "Stockholm",
"postal_code": "11122",
"payment_terms_days": 30,
"next_invoice_number": 15
}
```
#### PUT /api/settings
Updates company settings.
**Request:**
```json
{
"company_name": "Anna Andersson Content AB",
"municipal_tax_rate": 31.5,
"payment_terms_days": 14
}
```
---
### Transactions
#### GET /api/transactions
List transactions with optional filters.
**Query params:**
- `category`: filter by category
- `from`: start date (YYYY-MM-DD)
- `to`: end date
- `uncategorized`: boolean, only uncategorized
- `limit`: default 50
- `offset`: pagination
**Response 200:**
```json
{
"data": [
{
"id": "uuid",
"booking_date": "2024-01-15",
"amount": -1500.00,
"currency": "SEK",
"description": "ADOBE SYSTEMS",
"counterparty_name": "Adobe",
"category": "uncategorized",
"expense_type": null
}
],
"total": 142,
"uncategorized_count": 23
}
```
#### PATCH /api/transactions/:id
Categorize a transaction.
**Request:**
```json
{
"category": "business_expense",
"expense_type": "software",
"business_percentage": 100,
"notes": "Creative Cloud subscription"
}
```
#### POST /api/transactions/bulk-categorize
Categorize multiple transactions at once.
**Request:**
```json
{
"transaction_ids": ["uuid1", "uuid2"],
"category": "private"
}
```
---
### Banking
#### POST /api/banking/connect
Initiates PSD2 bank connection flow.
**Request:**
```json
{
"provider": "tink"
}
```
**Response 200:**
```json
{
"redirect_url": "https://link.tink.com/..."
}
```
#### GET /api/banking/callback
OAuth callback from banking provider. Handles token exchange.
**Query params:** Provider-specific (code, state, etc.)
**Redirects to:** `/transactions?connected=true`
#### POST /api/banking/sync
Manually trigger transaction sync. Also called by cron.
**Response 200:**
```json
{
"synced": 12,
"new_transactions": 3
}
```
---
### Customers
#### GET /api/customers
List all customers.
**Response 200:**
```json
{
"data": [
{
"id": "uuid",
"name": "Influencer Agency AB",
"customer_type": "swedish_business",
"org_number": "556677-8899",
"country": "SE",
"invoice_count": 5
}
]
}
```
#### POST /api/customers
Create new customer.
**Request:**
```json
{
"name": "Google Ireland Ltd",
"customer_type": "eu_business",
"vat_number": "IE6388047V",
"address_line1": "Gordon House",
"city": "Dublin",
"country": "IE",
"email": "payments@google.com"
}
```
**Response 201:** Created customer object
#### GET /api/customers/:id
Get single customer with invoice history.
#### PUT /api/customers/:id
Update customer.
#### DELETE /api/customers/:id
Soft delete (only if no invoices linked).
---
### VAT Validation
#### POST /api/vat/validate
Validate EU VAT number via VIES (VAT Information Exchange System).
**Critical for Reverse Charge:** Swedish law requires validated VAT number before applying 0% rate on EU B2B sales. Without validation, you must charge 25% Swedish VAT.
**Request:**
```json
{
"vat_number": "IE6388047V"
}
```
**Response 200:**
```json
{
"valid": true,
"country_code": "IE",
"vat_number": "6388047V",
"name": "GOOGLE IRELAND LIMITED",
"address": "GORDON HOUSE, BARROW STREET, DUBLIN 4",
"validated_at": "2024-01-15T10:30:00Z"
}
```
**Response 200 (Invalid):**
```json
{
"valid": false,
"country_code": "IE",
"vat_number": "INVALID123",
"error": "VAT number not found in VIES database"
}
```
**Implementation Notes:**
- Use EU VIES SOAP service or REST wrapper
- Cache validation for 24 hours (VAT numbers rarely change)
- Store `validated_at` timestamp on customer record
- Re-validate periodically for long-term customers
---
### Foreign Purchase Handling (Fiktiv Moms)
#### POST /api/transactions/:id/foreign-purchase
Mark a transaction as foreign service purchase requiring fiktiv moms.
When influencers buy services from abroad (Adobe CC, Facebook Ads, AWS), they must self-report VAT.
**Request:**
```json
{
"supplier_country": "US",
"service_type": "digital_service",
"vat_rate": 25
}
```
**Response 200:**
```json
{
"transaction_id": "uuid",
"fiktiv_moms_amount": 375.00,
"bookings": [
{ "account": "2645", "debit": 375.00, "description": "Beräknad ingående moms utländskt förvärv" },
{ "account": "2614", "credit": 375.00, "description": "Utgående moms utländskt förvärv" }
],
"moms_ruta_21": 1500.00,
"moms_ruta_48": 375.00
}
```
**Momsdeklaration Impact:**
- Ruta 21: Purchase amount (before VAT)
- Ruta 48: Self-reported output VAT
- Net effect is zero if input VAT is deductible, but both must be declared
```
---
### Invoices
#### GET /api/invoices
List invoices with filters.
**Query params:**
- `status`: draft, sent, paid, overdue
- `customer_id`: filter by customer
- `from`, `to`: date range
**Response 200:**
```json
{
"data": [
{
"id": "uuid",
"invoice_number": "INV-2024-015",
"customer": {
"id": "uuid",
"name": "Agency AB"
},
"invoice_date": "2024-01-15",
"due_date": "2024-02-14",
"total": 25000.00,
"currency": "SEK",
"status": "sent"
}
]
}
```
#### POST /api/invoices
Create new invoice.
**Request:**
```json
{
"customer_id": "uuid",
"invoice_date": "2024-01-15",
"due_date": "2024-02-14",
"reference": "PO-12345",
"items": [
{
"description": "Instagram kampanj November",
"quantity": 1,
"unit": "st",
"unit_price": 20000.00
}
],
"customer_notes": "Tack för samarbetet!"
}
```
The API automatically determines VAT treatment based on customer type:
```typescript
// lib/invoice/vat-rules.ts
interface VatDecision {
rate: number
reverseCharge: boolean
invoiceText: string | null
momsRuta: number // Which ruta in momsdeklaration
}
function determineVatTreatment(customer: Customer, settings: CompanySettings): VatDecision {
// User not VAT registered - no VAT on any invoice
if (!settings.vat_registered) {
return {
rate: 0,
reverseCharge: false,
invoiceText: 'Säljaren är inte momsregistrerad',
momsRuta: 0 // Not applicable
}
}
switch (customer.customer_type) {
case 'swedish_business':
case 'individual':
return {
rate: 25,
reverseCharge: false,
invoiceText: null,
momsRuta: 5 // Ruta 05: Momspliktig försäljning
}
case 'eu_business':
if (customer.vat_number_validated) {
return {
rate: 0,
reverseCharge: true,
invoiceText: 'Omvänd skattskyldighet / Reverse charge - Article 196 Council Directive 2006/112/EC',
momsRuta: 39 // Ruta 39: Tjänsteförsäljning EU
}
}
// EU business without valid VAT = charge Swedish VAT
return {
rate: 25,
reverseCharge: false,
invoiceText: null,
momsRuta: 5
}
case 'non_eu_business':
return {
rate: 0,
reverseCharge: false,
invoiceText: 'Export av tjänst - moms utgår ej',
momsRuta: 40 // Ruta 40: Export
}
}
}
```
**Momsdeklaration Ruta Reference:**
| Ruta | Description | When Used |
|------|-------------|-----------|
| 05 | Momspliktig försäljning | Swedish domestic sales |
| 39 | Tjänsteförsäljning EU | EU B2B with reverse charge |
| 40 | Export | Non-EU sales |
| 21 | Inköp av tjänster från EU | Foreign service purchases (input) |
| 48 | Utgående moms på inköp | Fiktiv moms on foreign purchases |
**EU Sales Reporting:**
Transactions with `momsRuta: 39` must be aggregated into Periodisk Sammanställning (quarterly report to Skatteverket), separate from regular momsdeklaration.
**Response 201:** Created invoice with calculated totals
#### GET /api/invoices/:id
Get full invoice details including items.
#### PUT /api/invoices/:id
Update draft invoice. Cannot modify sent/paid invoices.
#### POST /api/invoices/:id/send
Mark invoice as sent and optionally email to customer.
**Request:**
```json
{
"send_email": true,
"email_to": "invoice@client.com",
"email_message": "Hej! Här kommer fakturan för vårt samarbete."
}
```
**Email Implementation:** Uses Supabase built-in email via Edge Functions.
**Response 200:**
```json
{
"id": "uuid",
"status": "sent",
"sent_at": "2024-01-15T10:30:00Z",
"email_sent": true
}
```
**Note:** Invoice PDF attached to email. Payment method shown: bank transfer only (Bankgiro/IBAN). Future: third-party payment links (Stripe, Klarna).
#### POST /api/invoices/:id/mark-paid
Mark invoice as paid.
**Request:**
```json
{
"paid_at": "2024-02-10",
"paid_amount": 25000.00,
"linked_transaction_id": "uuid"
}
```
#### GET /api/invoices/:id/pdf
Generate and return invoice PDF.
**Query params:**
- `regenerate`: force regenerate cached PDF
**Response:** PDF file or redirect to signed storage URL
---
### Salary Payments (Aktiebolag only)
#### GET /api/salary
List salary payments for current fiscal year.
**Response 200:**
```json
{
"data": [
{
"id": "uuid",
"payment_date": "2024-01-25",
"pay_period_start": "2024-01-01",
"pay_period_end": "2024-01-31",
"gross_salary": 50000.00,
"employer_contributions": 15710.00,
"withheld_tax": 15000.00,
"net_salary": 35000.00,
"total_cost": 65710.00,
"agi_reported": false
}
],
"ytd_totals": {
"gross_salary": 50000.00,
"employer_contributions": 15710.00,
"withheld_tax": 15000.00,
"total_cost": 65710.00
}
}
```
#### POST /api/salary
Create salary payment record.
**Request:**
```json
{
"pay_period_start": "2024-01-01",
"pay_period_end": "2024-01-31",
"gross_salary": 50000.00,
"tax_table": 33
}
```
**Automatic calculations:**
- `employer_contributions`: gross × 31.42%
- `withheld_tax`: from Swedish tax tables based on gross + table number
- `net_salary`: gross - withheld_tax
- `total_cost`: gross + employer_contributions
**Response 201:** Created salary payment with all calculated fields.
#### POST /api/salary/:id/mark-agi-reported
Mark salary payment as reported in AGI-deklaration.
**Response 200:**
```json
{
"id": "uuid",
"agi_reported": true,
"agi_reported_at": "2024-02-12T14:00:00Z"
}
```
---
### Dashboard
#### GET /api/dashboard/summary
Get aggregated financial overview.
**Response 200:**
```json
{
"period": {
"year": 2024,
"month": 1
},
"revenue": {
"ytd": 450000.00,
"this_month": 75000.00
},
"expenses": {
"ytd": 85000.00,
"this_month": 12000.00
},
"net_income": {
"ytd": 365000.00
},
"tax_estimate": {
"egenavgifter": 105745.00,
"income_tax": 116800.00,
"vat_payable": 91250.00,
"total_locked": 313795.00
},
"disponibelt": 136205.00,
"vat_threshold": {
"limit": 80000,
"current": 450000.00,
"registered": true
},
"pending": {
"uncategorized_transactions": 23,
"unpaid_invoices": 2,
"unpaid_amount": 45000.00
}
}
```
#### GET /api/dashboard/chart
Monthly revenue/expense data for charts.
**Query params:**
- `months`: number of months (default 12)
**Response 200:**
```json
{
"data": [
{
"month": "2024-01",
"revenue": 75000,
"expenses": 12000,
"net": 63000
}
]
}
```
---
## Error Responses
All endpoints return errors in consistent format:
```json
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid VAT number format",
"details": {
"field": "vat_number",
"value": "invalid"
}
}
}
```
**Common status codes:**
- 400: Validation error
- 401: Not authenticated
- 403: Forbidden (RLS violation)
- 404: Resource not found
- 409: Conflict (duplicate invoice number)
- 500: Internal error