Initial copy from influencer-biz

This commit is contained in:
Jakob Wennberg
2026-02-13 14:00:46 +01:00
commit a25d10a528
391 changed files with 96224 additions and 0 deletions
+672
View File
@@ -0,0 +1,672 @@
# 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