673 lines
13 KiB
Markdown
673 lines
13 KiB
Markdown
# 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
|