Initial copy from influencer-biz
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user