Craft Easy — Financial Ecosystem Specification¶
Version: 1.0 Date: 2026-03-28 Status: Approved Related: advanced-features-specification.md
Contents¶
- Overview — the money flow
- Multi-currency
- Tenant agreements
- Revenue split (tenant, system owner, partner)
- Invoicing
- Subcontractors (underleverantörer)
- Data models
- Flows and examples
- Implementation phases
1. Overview — The Money Flow¶
The financial ecosystem involves multiple parties and multiple directions of money:
┌────────────────────────────────────────────────────────────────────────┐
│ SYSTEM OWNER (you) │
│ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Craft Easy Platform │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ Tenant A │ │ Tenant B │ │ Tenant C │ │ │
│ │ │ (Company) │ │ (Consortium)│ │ (Solo) │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ Agreement: │ │ Agreement: │ │ Agreement: │ │ │
│ │ │ 80/15/5 │ │ 70/20/10 │ │ 90/10/0 │ │ │
│ │ │ T/Sys/Part │ │ T/Sys/Part │ │ T/Sys │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ Customers │ │ Companies │ │ Customers │ │ │
│ │ │ ├── Cust 1 │ │ ├── Org 1 │ │ ├── Cust X │ │ │
│ │ │ ├── Cust 2 │ │ │ └─ Cust │ │ └── Cust Y │ │ │
│ │ │ └── Cust 3 │ │ └── Org 2 │ │ │ │ │
│ │ │ │ │ └─ Cust │ │ │ │ │
│ │ │ Subcontr. │ │ │ │ │ │ │
│ │ │ └── Sub X │ │ Partner: │ │ │ │ │
│ │ │ │ │ Partner AB │ │ │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ └──────────────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────────┘
Money flows:
─────────────
Customer → pays → Tenant (via Stripe/Klarna/Swish)
Tenant → revenue split per agreement → System Owner + Partner
System Owner → invoices Tenant → feature/service fees
Tenant → invoices Customers → for services rendered
Tenant → pays → Subcontractor (for work done)
The parties¶
| Party | Role | Examples |
|---|---|---|
| System Owner | Operates the platform | You (Easy Software System) |
| Tenant | Customer of the platform | A parking company, a healthcare provider |
| Partner | Referred the tenant, gets revenue share | Sales partner, franchise holder |
| Customer | Tenant's end customer | Person parking a car, patient booking appointment |
| Subcontractor | Does work for the tenant | Cleaning company, maintenance provider |
2. Multi-Currency¶
2.1 Requirements¶
- Each tenant can operate in one or more currencies
- Transactions, invoices, and settlements are per-currency
- New currencies can be added at any time without migration
- Exchange rates are NOT handled by the system (each transaction is in a specific currency)
2.2 Currency model¶
class Currency(BaseDocument):
"""Supported currency. System-wide, not per tenant."""
code: str # ISO 4217: "SEK", "EUR", "USD", "NOK"
name: str # "Swedish Krona"
symbol: str # "kr", "€", "$"
decimal_places: int = 2
is_enabled: bool = True
class TenantConfig:
tenant_scoped = False # Global
class Settings:
name = "currencies"
2.3 Multi-currency on tenant¶
class Tenant(BaseDocument):
# ... existing fields
default_currency: str = "SEK" # ISO 4217
enabled_currencies: list[str] = ["SEK"] # Can add EUR, USD, etc.
2.4 All monetary fields use currency explicitly¶
class MoneyAmount(BaseModel):
"""Every monetary value carries its currency."""
amount: Decimal
currency: str # "SEK"
No implicit currency anywhere. Every amount has a currency next to it.
2.5 Payment accounts (BG, PG, bank accounts)¶
Each tenant can either use their own payment accounts or use the system owner's. This determines where customer payments land and how they flow through the system.
class PaymentAccount(BaseDocument):
"""A bankgiro, plusgiro, or bank account that receives payments."""
owner_type: str # "system_owner" | "tenant"
owner_id: Optional[PydanticObjectId] = None # None for system owner
type: str # "bankgiro" | "plusgiro" | "bank_account" | "iban"
account_number: str # "123-4567" (BG) | "12 34 56-7" (PG) | IBAN
bank_name: Optional[str] = None
currency: str = "SEK"
is_default: bool = False
is_enabled: bool = True
# Who uses this account
used_by_tenants: list[PydanticObjectId] = [] # If system owner account shared with tenants
class TenantConfig:
tenant_scoped = False # Scoped by owner_type/owner_id
class Settings:
name = "payment_accounts"
Two modes per tenant¶
| Mode | How it works | Money flow |
|---|---|---|
| Own accounts | Tenant has their own BG/PG. Customer pays tenant directly. | Customer → Tenant's BG → Tenant settles with system owner |
| System owner's accounts | Tenant uses system owner's BG/PG. Customer pays system owner. | Customer → System owner's BG → System owner settles with tenant |
Configured on the tenant agreement:
class TenantAgreement(BaseDocument):
# ... existing fields
# --- Payment accounts ---
payment_account_mode: str # "own" | "system_owner"
payment_accounts: list[PydanticObjectId] = [] # Tenant's own accounts (if mode = "own")
# If mode = "system_owner", the system owner's default accounts are used
Impact on settlement¶
| Mode | Who holds the money | Settlement direction |
|---|---|---|
| own | Tenant | Tenant → pays system owner's share + partner's share |
| system_owner | System owner | System owner → pays tenant's share + partner's share |
Mode "own" (tenant has BG):
Customer pays 1000 → Tenant's BG
Settlement: Tenant owes System Owner 150 + Partner 50
Tenant pays out 200
Mode "system_owner" (tenant uses system owner's BG):
Customer pays 1000 → System Owner's BG
Settlement: System Owner owes Tenant 800 + Partner 50
System Owner pays out 850
Payment file matching¶
When a payment file arrives, the system determines which tenant it belongs to:
# Payment file from System Owner's BG:
# → Match reference/OCR to claim → claim has tenant_id → route to correct tenant
# Payment file from Tenant's own BG:
# → File import config is tenant-scoped → already knows the tenant
3. Tenant Agreements¶
3.1 Concept¶
Every tenant has an agreement that governs: - Which features/modules are enabled - Revenue split percentages - Service fees (recurring charges for using the platform) - Billing cycle - Payment terms
3.2 Agreement model¶
class TenantAgreement(BaseDocument):
"""Contract between system owner and tenant."""
tenant_id: PydanticObjectId
name: str # "Standard Agreement", "Enterprise Deal"
status: str # "draft" | "active" | "terminated"
valid_from: datetime
valid_until: Optional[datetime] = None
# --- Feature access ---
enabled_features: list[str] = []
# e.g. ["payments", "invoicing", "bi-export", "gdpr", "client-funds"]
# --- Revenue split ---
revenue_splits: list[RevenueSplitRule] = []
# --- Service fees (platform charges to tenant) ---
service_fees: list[ServiceFee] = []
# --- Settlement order (avräkningsordning) ---
settlement_orders: list[SettlementOrder] = []
# --- Billing ---
billing_cycle: str = "monthly" # "monthly" | "quarterly" | "annually"
payment_terms_days: int = 30
billing_currency: str = "SEK"
self_billing: bool = False # True = system owner issues invoice on behalf of tenant
# --- Partner ---
partner_id: Optional[PydanticObjectId] = None
partner_name: Optional[str] = None
class TenantConfig:
tenant_scoped = True
class Settings:
name = "tenant_agreements"
class RevenueSplitRule(BaseModel):
"""How revenue is split for a specific category/product."""
category: str # "all" | "parking" | "subscriptions" | specific product
tenant_percentage: Decimal # 80.00
system_owner_percentage: Decimal # 15.00
partner_percentage: Decimal # 5.00
# Must sum to 100.00
def validate_sum(self) -> bool:
return (
self.tenant_percentage
+ self.system_owner_percentage
+ self.partner_percentage
) == Decimal("100.00")
class ServiceFee(BaseModel):
"""Recurring fee charged by system owner to tenant."""
name: str # "Platform fee", "API access", "Premium support"
type: str # "fixed" | "per_user" | "per_transaction" | "percentage"
amount: Decimal # Fixed: 999.00, Per user: 49.00, Percentage: 2.50
currency: str = "SEK"
billing_cycle: str = "monthly" # Can differ from agreement cycle
is_taxable: bool = True
vat_rate: Decimal = Decimal("0.25") # 25%
3.3 Settlement order (avräkningsordning)¶
When a payment comes in, the settlement order defines in which order cost types are paid off. This can vary by product category or by where in the collection process the claim is.
class SettlementOrder(BaseModel):
"""Defines the order in which cost types are settled when payment is received."""
name: str # "Standard", "Collection phase", "Enforcement phase"
# When does this order apply?
applies_to: SettlementOrderScope
# The ordered list — payment is applied top to bottom
order: list[SettlementOrderLine]
class SettlementOrderScope(BaseModel):
"""Defines when a settlement order applies."""
product_categories: list[str] = ["all"] # ["parking", "subscriptions"] or ["all"]
collection_stages: list[str] = ["all"] # ["normal", "reminder", "collection", "enforcement"] or ["all"]
# Most specific match wins: product+stage > product > stage > all
class SettlementOrderLine(BaseModel):
"""One step in the settlement order."""
cost_type: str # References CostType.code: "capital", "interest", "collection_fee"
priority: int # 1 = first, 2 = second, etc.
max_percentage: Optional[Decimal] = None # Optional cap: "max 50% of payment to interest"
Example: Standard order (normal claims)
| Priority | Cost type | Description |
|---|---|---|
| 1 | enforcement_fee |
Kronofogdeavgift — always first if applicable |
| 2 | collection_fee |
Inkassoavgift |
| 3 | reminder_fee |
Påminnelseavgift |
| 4 | interest |
Ränta |
| 5 | invoice_fee |
Fakturaavgift |
| 6 | capital |
Kapital — last |
Example: Early collection order (reminder phase)
| Priority | Cost type |
|---|---|
| 1 | reminder_fee |
| 2 | interest |
| 3 | capital |
Example: Subscription product order (different from standard)
| Priority | Cost type |
|---|---|
| 1 | capital |
| 2 | interest |
| 3 | invoice_fee |
This way: - Each agreement can have multiple settlement orders - Different products can have different orders - Different collection stages can have different orders - Payment is applied line by line, top to bottom, until exhausted
class SettlementOrderService:
@staticmethod
async def apply_payment(
tenant_id: PydanticObjectId,
claim_id: PydanticObjectId,
payment_amount: Decimal,
currency: str,
) -> list[SettlementAllocation]:
"""
Apply a payment to a claim using the settlement order from the agreement.
Returns list of allocations showing how the payment was distributed.
"""
claim = await Claim.get(claim_id)
agreement = await _get_active_agreement(tenant_id)
# Find matching settlement order
order = _find_matching_order(
agreement.settlement_orders,
product_category=claim.product_category,
collection_stage=claim.collection_stage,
)
remaining = payment_amount
allocations = []
for line in sorted(order.order, key=lambda l: l.priority):
if remaining <= 0:
break
outstanding = claim.outstanding_by_cost_type.get(line.cost_type, Decimal("0"))
if outstanding <= 0:
continue
# Apply cap if configured
allocatable = outstanding
if line.max_percentage:
cap = payment_amount * line.max_percentage / 100
allocatable = min(outstanding, cap)
allocated = min(remaining, allocatable)
remaining -= allocated
allocations.append(SettlementAllocation(
cost_type=line.cost_type,
amount=allocated,
currency=currency,
))
return allocations
class SettlementAllocation(BaseModel):
"""How a portion of a payment was allocated."""
cost_type: str
amount: Decimal
currency: str
3.4 Feature gating via agreement¶
The agreement controls which features a tenant can access:
# In access control middleware:
async def check_tenant_feature(tenant_id: str, feature: str) -> bool:
agreement = await TenantAgreement.find_one(
TenantAgreement.tenant_id == tenant_id,
TenantAgreement.status == "active",
)
if not agreement:
return False
return feature in agreement.enabled_features
This is SEPARATE from user-level access control: - Agreement controls what the TENANT can do (module-level) - AccessGroup controls what a USER within the tenant can do (feature-level)
Agreement: Tenant has "payments" module enabled
└── AccessGroup: User "Anna" has "payments.create" and "payments.refund"
└── AccessGroup: User "Erik" has only "payments.list" (read-only)
4. Revenue Split (Tenant, System Owner, Partner)¶
4.1 How it works¶
When a customer pays, the revenue is split according to the tenant's agreement:
Customer pays 1000 SEK
│
▼ Agreement: 80/15/5 (Tenant/System/Partner)
│
├── Tenant: 800 SEK
├── System Owner: 150 SEK
└── Partner: 50 SEK
4.2 Split calculation service¶
class RevenueSplitService:
@staticmethod
async def calculate_split(
tenant_id: PydanticObjectId,
amount: Decimal,
currency: str,
category: str = "all",
) -> list[SplitResult]:
"""Calculate revenue split based on tenant agreement."""
agreement = await TenantAgreement.find_one(
TenantAgreement.tenant_id == tenant_id,
TenantAgreement.status == "active",
)
# Find matching rule (specific category first, then "all")
rule = _find_matching_rule(agreement.revenue_splits, category)
return [
SplitResult(party="tenant", party_id=tenant_id,
amount=amount * rule.tenant_percentage / 100),
SplitResult(party="system_owner", party_id=None,
amount=amount * rule.system_owner_percentage / 100),
SplitResult(party="partner", party_id=agreement.partner_id,
amount=amount * rule.partner_percentage / 100),
]
4.3 Different splits per product category¶
A tenant agreement can have different splits for different products:
agreement.revenue_splits = [
RevenueSplitRule(
category="parking",
tenant_percentage=Decimal("80.00"),
system_owner_percentage=Decimal("15.00"),
partner_percentage=Decimal("5.00"),
),
RevenueSplitRule(
category="subscriptions",
tenant_percentage=Decimal("70.00"),
system_owner_percentage=Decimal("25.00"),
partner_percentage=Decimal("5.00"),
),
RevenueSplitRule(
category="all", # Fallback
tenant_percentage=Decimal("75.00"),
system_owner_percentage=Decimal("20.00"),
partner_percentage=Decimal("5.00"),
),
]
4.4 Claims (fordringar)¶
A claim tracks what is owed, broken down by cost type, with collection stage tracking:
class Claim(BaseDocument):
"""A claim/receivable — what a customer owes, broken down by cost type."""
tenant_id: PydanticObjectId
customer_id: PydanticObjectId
customer_name: Optional[str] = Field(json_schema_extra={"readonly": True})
# What generated the claim
source_type: str # "invoice" | "booking" | "subscription" | "manual"
source_id: Optional[PydanticObjectId] = None
product_category: str = "all" # Matches settlement order scope
# Collection process
collection_stage: str = "normal" # "normal" | "reminder" | "collection" | "enforcement"
collection_history: list[CollectionEvent] = []
# Amounts by cost type
cost_lines: list[ClaimCostLine] = []
total_amount: Decimal # Sum of all cost lines
paid_amount: Decimal = Decimal("0.00")
outstanding_amount: Decimal # total - paid
currency: str
due_date: datetime
status: str # "open" | "partially_paid" | "paid" | "written_off"
class TenantConfig:
tenant_scoped = True
class Settings:
name = "claims"
@property
def outstanding_by_cost_type(self) -> dict[str, Decimal]:
return {
line.cost_type: line.amount - line.paid_amount
for line in self.cost_lines
if line.amount > line.paid_amount
}
class ClaimCostLine(BaseModel):
"""One cost component of a claim."""
cost_type: str # "capital", "interest", "reminder_fee", etc.
description: str
amount: Decimal
paid_amount: Decimal = Decimal("0.00")
added_at: datetime # When this cost was added
class CollectionEvent(BaseModel):
"""Tracks progression through collection stages."""
stage: str # "reminder" | "collection" | "enforcement"
date: datetime
description: str
fee_added: Optional[Decimal] = None # Fee added at this stage
cost_type: Optional[str] = None # Which cost type the fee is
Flow: a claim progresses through collection
1. Invoice created → Claim (stage: normal, costs: [capital: 1000])
2. Due date passes → no payment
3. Reminder sent → stage: reminder, costs: [capital: 1000, reminder_fee: 60]
4. Still unpaid after 14 days
5. Sent to collection → stage: collection, costs: [..., collection_fee: 180]
6. Still unpaid
7. Sent to enforcement → stage: enforcement, costs: [..., enforcement_fee: 600]
8. Payment of 500 received:
- Apply settlement order for "enforcement" stage:
1. enforcement_fee: 500 → allocate 500, remaining 0
- Result: enforcement_fee partially paid
9. Payment of 1340 received:
- enforcement_fee: remaining 100 → allocate 100, remaining 1240
- collection_fee: 180 → allocate 180, remaining 1060
- reminder_fee: 60 → allocate 60, remaining 1000
- interest: 0 (none added yet)
- capital: 1000 → allocate 1000, remaining 0
- Claim fully paid
5. Invoicing¶
5.1 Invoice directions¶
| From | To | Type | Example |
|---|---|---|---|
| System Owner → Tenant | Service fees | "Platform fee March 2026: 4 999 SEK" | |
| Tenant → Customer | Service/product charges | "Parking booking #123: 299 SEK" | |
| System Owner → Tenant (self-billing) | Revenue share settlement | "Your share of March revenue: 45 000 SEK" |
5.2 Self-billing (självfakturering)¶
When agreement.self_billing = True, the system owner issues invoices on behalf of the tenant for the system owner's share of revenue. This is common in marketplace models.
Flow: 1. Settlement calculates system owner's share: 15 000 SEK 2. System generates a self-billing invoice: "Tenant A owes System Owner 15 000 SEK" 3. Amount is deducted from tenant's payout
5.3 Recurring service fee invoicing¶
class InvoiceScheduler:
"""Generates recurring invoices based on tenant agreements."""
@staticmethod
async def generate_service_fee_invoices(billing_period: str):
"""Run monthly/quarterly — generates invoices for all active agreements."""
agreements = await TenantAgreement.find(
TenantAgreement.status == "active",
).to_list()
for agreement in agreements:
for fee in agreement.service_fees:
if fee.billing_cycle == billing_period:
await _create_service_invoice(agreement, fee)
5.4 Invoice model¶
class Invoice(BaseDocument):
"""An invoice — can be from any party to any party."""
invoice_number: str # Auto-generated, sequential per tenant
type: str # "service_fee" | "customer_charge" | "self_billing" | "credit_note"
status: str # "draft" | "sent" | "paid" | "overdue" | "cancelled" | "credited"
# Parties
issuer_tenant_id: Optional[PydanticObjectId] = None # None = system owner
issuer_name: str
issuer_address: Optional[str] = None
issuer_org_number: Optional[str] = None
issuer_vat_number: Optional[str] = None
recipient_tenant_id: Optional[PydanticObjectId] = None
recipient_customer_id: Optional[PydanticObjectId] = None
recipient_name: str
recipient_address: Optional[str] = None
recipient_org_number: Optional[str] = None
# Amounts
currency: str
lines: list[InvoiceLine]
subtotal: Decimal
vat_amount: Decimal
total: Decimal
# Dates
invoice_date: datetime
due_date: datetime
paid_date: Optional[datetime] = None
# Payment
payment_reference: Optional[str] = None # OCR, reference number
payment_method: Optional[str] = None
# Related
agreement_id: Optional[PydanticObjectId] = None
settlement_id: Optional[PydanticObjectId] = None
class TenantConfig:
tenant_scoped = True
class Settings:
name = "invoices"
class InvoiceLine(BaseModel):
"""One line on an invoice."""
description: str
quantity: Decimal = Decimal("1")
unit_price: Decimal
vat_rate: Decimal = Decimal("0.25")
amount: Decimal # quantity * unit_price
cost_type: Optional[str] = None # Links to CostType
6. Subcontractors (Underleverantörer)¶
6.1 Concept¶
A tenant can have subcontractors that: - See a subset of the tenant's data (scoped access) - Perform work that generates costs/revenue - May need to be paid as part of settlement
6.2 Subcontractor model¶
class Subcontractor(BaseDocument):
"""A subcontractor that works for a tenant."""
tenant_id: PydanticObjectId
name: str
org_number: Optional[str] = None
contact_email: Optional[str] = None
is_enabled: bool = True
# Access: which resources and tags can the subcontractor see
data_scope: SubcontractorScope
class TenantConfig:
tenant_scoped = True
class Settings:
name = "subcontractors"
class SubcontractorScope(BaseModel):
"""Defines what data a subcontractor can access."""
resources: list[str] = [] # ["bookings", "customers"]
tag_filters: list[TagFilter] = [] # Only see items with these tags
methods: list[str] = ["GET"] # Usually read-only
class TagFilter(BaseModel):
"""Tag-based filter for scoping."""
category: str # "location"
include_tags: list[PydanticObjectId] = []
exclude_tags: list[PydanticObjectId] = []
6.3 Subcontractor authentication¶
Subcontractors get their own user accounts with a special access group that limits them to their SubcontractorScope. The tag filters automatically apply to all queries.
7. Data Models — Complete Picture¶
7.1 Entity relationship overview¶
Tenant
├── TenantAgreement (1:1 active)
│ ├── RevenueSplitRule (N)
│ ├── ServiceFee (N)
│ └── Partner reference
│
├── Users (N)
│ └── AccessGroups (N)
│ └── Features (N)
│
├── Tags (N, hierarchical)
│ └── Used by any document for filtering
│
├── Subcontractors (N)
│ └── SubcontractorScope
│
├── Customers (N) — tenant's end customers
│
├── Invoices (N)
│ ├── System Owner → Tenant (service fees)
│ ├── Tenant → Customer (charges)
│ └── Self-billing (system owner's share)
│
├── Payments (N) — from customers
│ └── Revenue splits calculated per agreement
│
├── Settlements (N) — periodic reconciliation
│ └── Settlement lines (per party)
│
├── ClientFundsAccount (0-1)
│ └── ClientFundsTransactions (N)
│
├── Accounts (bookkeeping) (N)
│ └── JournalEntries (N)
│
└── CostTypes (N) — configurable cost categories
7.2 Currency flow¶
Customer pays 1000 SEK
│
┌──────▼──────┐
│ Payment │
│ 1000 SEK │
└──────┬──────┘
│
┌──────▼──────┐
│ Revenue Split│
│ (Agreement) │
└──┬───┬───┬──┘
│ │ │
┌────────┘ │ └────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Tenant │ │ System │ │ Partner │
│ 800 SEK │ │ 150 SEK │ │ 50 SEK │
└──────────┘ └──────────┘ └──────────┘
│ │
│ ▼
│ ┌──────────┐
│ │ Self-bill│
│ │ invoice │
│ │ 150 SEK │
│ └──────────┘
│
▼
┌──────────┐
│Settlement │
│ Payout: │
│ 800 SEK │
│ - fees │
│ = net │
└──────────┘
7.3 Multi-currency example¶
Tenant A operates in both SEK and EUR:
SEK transactions → settled in SEK → paid out in SEK
EUR transactions → settled in EUR → paid out in EUR
NO cross-currency conversion within the system.
Each currency has its own settlement cycle.
8. Flows and Examples¶
8.0 Flow: Payment received directly to tenant (file import)¶
When a tenant receives payments directly (not through Stripe/Klarna) — e.g. bank transfer, Bankgirot — these arrive as payment files and must be imported, matched, and reconciled:
1. Bank deposits payment to tenant's account
2. Bank generates payment file (Bankgirot, SEPA, etc.)
3. craft-easy-file-import polls SFTP → detects file
4. Parses file → list of payments with reference + amount
5. For each payment:
a. Match reference to outstanding claim
b. Apply settlement order (avräkningsordning from agreement)
c. Allocate: enforcement_fee → collection_fee → reminder_fee → interest → capital
d. Create journal entries in tenant's books
e. Calculate revenue split per agreement
f. Create journal entries in system owner's + partner's books
g. Update claim status (partially_paid / paid)
6. Unmatched payments → flagged for manual review
7. Revenue from matched payments included in next settlement run
This is the connection between craft-easy-file-import and the financial ecosystem. Full spec: file-import-specification.md section 7.
8.1 Flow: Customer pays for parking¶
1. Customer books parking (299 SEK)
2. Payment via Stripe → Payment record created
3. Revenue split calculated:
- Tenant: 239.20 SEK (80%)
- System Owner: 44.85 SEK (15%)
- Partner: 14.95 SEK (5%)
4. Split recorded in journal entries
5. End of month: settlement run
6. Settlement invoice generated (self-billing)
7. Payout to tenant: 239.20 SEK - service fees
8. System owner keeps: 44.85 SEK + service fees
9. Partner payout: 14.95 SEK
8.2 Flow: Monthly service fee billing¶
1. 1st of month: InvoiceScheduler runs
2. For each active agreement with monthly fees:
- Platform fee: 4 999 SEK
- Per-user fee: 49 SEK × 15 users = 735 SEK
- Total: 5 734 SEK + VAT
3. Invoice created: System Owner → Tenant
4. Invoice sent (email/PDF)
5. Payment tracked
8.3 Flow: Tenant invoices their customer¶
1. Tenant creates invoice to Customer (via API or admin)
2. Invoice lines reference cost types
3. Payment tracking:
- Customer pays → mark as paid
- Customer doesn't pay → reminder fee added
- Still unpaid → collection fee
- Still unpaid → enforcement fee
4. All fees follow configurable cost types
8.4 Flow: Subcontractor access¶
1. Tenant adds subcontractor "Clean Co"
2. Scope: resource "bookings", tags "stockholm", methods ["GET"]
3. Clean Co user logs in → can only see:
- Bookings tagged "stockholm"
- Read-only
4. Clean Co completes work → Tenant logs it
5. Settlement includes subcontractor payment
9. Implementation Phases¶
This is an extension of the phases in advanced-features-specification.md:
| Phase | Module | Weeks | Depends on |
|---|---|---|---|
| 6 | Multi-currency support | 1 | Multi-tenant (Phase 1) |
| 6 | Bookkeeping (accounts, journal entries, cost types) | 2 | Multi-tenant |
| 7 | Client funds (Sweden, extensible) | 1 | Bookkeeping |
| 8a | Payment providers (abstract + Stripe) | 2 | Multi-tenant |
| 8b | Payment providers (Klarna + Swish) | 1 | Phase 8a |
| 9 | Tenant agreements (features, splits, fees) | 2 | Multi-tenant |
| 10 | Revenue split service | 1 | Agreements + Payments |
| 11 | Invoicing (all directions + self-billing) | 2 | Agreements + Bookkeeping |
| 12 | Settlement (calculation, approval, payout) | 2 | Revenue split + Invoicing |
| 13 | Subcontractor access + scoping | 1 | Tags + Access control |
| 14 | Recurring fee invoicing (scheduler) | 1 | Invoicing + Agreements |
Total: ~16 additional weeks for the full financial ecosystem.
What to build first¶
The financial modules depend on the core modules from the advanced spec:
Phase 0: Remove fixed hierarchy ← Already decided
Phase 1: Multi-tenant ← Foundation for everything
Phase 2: Tags ← Needed for subcontractor scoping
Phase 3: GDPR ← Needed for BI export
Phase 4: White label ← Independent
Phase 5: BI export ← Needs GDPR
Phase 6: Multi-currency + Bookkeeping ← START of financial ecosystem
Phase 7: Client funds
Phase 8: Payments (Stripe, Klarna, Swish)
Phase 9: Agreements
Phase 10: Revenue splits
Phase 11: Invoicing
Phase 12: Settlement
Phase 13: Subcontractors
Phase 14: Recurring billing