Skip to content

Revenue Splits

Every customer payment a tenant receives can be split between multiple parties: the platform (you), the tenant, and optionally a partner (a reseller, a referrer, an affiliate). Revenue splits encode the rules — percentage, fixed, or tiered — as date-ranged records so that historical settlements use the rule that applied when the revenue was earned, not whichever rule is active today.

Splits sit between Payments and Settlements. A payment arrives, the split service computes each party's share, and the settlement service packages those shares into payouts.

The two models

RevenueSplitRule — the configuration

craft_easy.models.revenue_split.RevenueSplitRule:

Field Purpose
agreement_id Link to the tenant agreement this rule belongs to
tenant_id Tenant scope
split_type percentage, fixed, or tiered
platform_share Platform's slice (0-100) — used by percentage and tiered
owner_share Tenant's slice (0-100) — the "owner" here is the tenant owner
platform_fixed Fixed platform cut in currency units — used by fixed
currency ISO 4217
tier_rules List of TierRule — used by tiered
valid_from, valid_to Date range during which this rule applies
vat_rate VAT rate in percent (e.g. 25 for 25%)
split_on_net If true, split is computed on the net (ex-VAT) amount. If false, split is computed on the gross amount. Default True

A TierRule describes a volume break:

TierRule(
    min_revenue=Decimal("0"),
    max_revenue=Decimal("10000"),      # or None for unlimited
    platform_share=Decimal("30"),
    owner_share=Decimal("70"),
    fixed_amount=None,                 # alternative to percentage shares
)

Multiple rules can exist for a single tenant — each with a different date range. The service picks the one whose valid_from <= today < valid_to at the moment of calculation. Rules never overlap; creating an overlapping rule is rejected at the service layer.

RevenueSplitRecord — the historical output

craft_easy.models.revenue_split.RevenueSplitRecord is immutable (no revisions). Once a record is written, its fields are frozen — amending a split requires a new record with a pointer to the original.

Field Purpose
tenant_id Tenant scope
rule_id Which rule produced this record
payment_id Link to the Payment that was split (optional — aggregate records omit this)
gross_revenue Pre-VAT gross amount
vat_amount VAT extracted
net_revenue gross_revenue - vat_amount
platform_amount Platform's share after the split
owner_amount Tenant's share
currency ISO 4217
period_from, period_to Period the record covers (single payment or aggregate)
calculation_details Free-form dict explaining how the split was computed — useful for audit and customer support

The record is what the settlement service reads to produce payouts. Every line item on a Settlement traces back to one of these records.

RevenueSplitService

craft_easy.core.revenue.service.RevenueSplitService:

from craft_easy.core.revenue.service import RevenueSplitService

svc = RevenueSplitService()

Finding the active rule

rule = await svc.get_active_rule(tenant_id, as_of=date(2026, 4, 5))
# or
rule = await svc.get_active_rule(tenant_id)  # defaults to today

Returns the rule whose valid_from <= as_of and (valid_to is None or as_of < valid_to). If no rule matches, returns None. A tenant with no active rule cannot receive splits — the settlement service treats such tenants as receiving 100% of their revenue (no platform cut), which is usually wrong, so monitor for missing rules via a health check.

Calculating a single split

split = svc.calculate_split(rule, gross_revenue=Decimal("10000"))
# Returns:
# {
#   "gross_revenue": Decimal("10000"),
#   "vat_amount": Decimal("2000"),
#   "net_revenue": Decimal("8000"),
#   "platform_amount": Decimal("2400"),   # 30% of 8000
#   "owner_amount": Decimal("5600"),       # 70% of 8000
#   "currency": "SEK",
#   "calculation_details": {...}
# }

The service computes:

  1. VAT extraction (if vat_rate > 0): vat_amount = gross_revenue * vat_rate / (100 + vat_rate). That is the reverse-charge formula — extract VAT from a gross amount — which is the right direction for a customer-facing charge.
  2. Net revenue: net_revenue = gross_revenue - vat_amount.
  3. Split basis: basis = net_revenue if rule.split_on_net else gross_revenue.
  4. Split computation per split_type:
  5. percentage: platform = basis * platform_share / 100, owner = basis * owner_share / 100.
  6. fixed: platform = min(rule.platform_fixed, basis), owner = basis - platform. The min clamp prevents a fixed fee from exceeding the basis on tiny payments.
  7. tiered: walks tier_rules looking for the tier where min_revenue <= basis < max_revenue (or max_revenue is None). Applies that tier's platform_share/owner_share. This is a lookup per payment, not a progressive calculation across tiers — the matched tier's rate is applied to the entire basis.

Calculating a period

aggregate = await svc.calculate_for_period(
    tenant_id,
    from_date=date(2026, 4, 1),
    to_date=date(2026, 5, 1),
)
# Returns:
# {
#   "gross_revenue": Decimal("..."),
#   "platform_amount": Decimal("..."),
#   "owner_amount": Decimal("..."),
#   "currency": "SEK",
#   "payments": [ {payment_id, gross, platform, owner}, ... ],
# }

The period calculation walks every Payment in the period that belongs to the tenant, looks up the active rule per payment (so a rule change mid-period is respected), and sums the result. Per-payment detail is returned alongside the aggregate for drill-down.

The period calculation is what the settlement runner calls at the end of each billing period. It does not persist the results on its own — that is the settlement service's job, which calls calculate_for_period and writes one SettlementLineItem per payment plus a RevenueSplitRecord per payment as an audit trail.

Three split types in practice

Percentage

The simplest case. "Platform gets 30%, tenant gets 70%." Works well when the margin is stable and the platform cost scales linearly with revenue.

rule = RevenueSplitRule(
    agreement_id=agreement.id,
    tenant_id=tenant.id,
    split_type="percentage",
    platform_share=Decimal("30"),
    owner_share=Decimal("70"),
    currency="SEK",
    valid_from=date(2026, 1, 1),
    valid_to=None,
    vat_rate=Decimal("25"),
    split_on_net=True,
)

Fixed

"Platform gets 50 SEK per transaction, tenant gets the rest." Works for low-margin high-volume businesses where the platform cost is essentially a per-transaction processing fee.

rule = RevenueSplitRule(
    agreement_id=agreement.id,
    tenant_id=tenant.id,
    split_type="fixed",
    platform_fixed=Decimal("50"),
    currency="SEK",
    valid_from=date(2026, 1, 1),
    valid_to=None,
    vat_rate=Decimal("25"),
    split_on_net=True,
)

Watch the clamp: on a basis=30 SEK transaction, the platform receives 30 SEK and the tenant receives 0. That is usually the right behavior (the platform cost is a floor), but if your deployment has tiny-payment edge cases, consider enforcing a minimum transaction size at payment creation.

Tiered

"Platform gets 30% up to 10000 SEK, 20% between 10k and 50k, 15% above 50k." Works when the platform wants to incentivize high-volume tenants with better terms.

rule = RevenueSplitRule(
    agreement_id=agreement.id,
    tenant_id=tenant.id,
    split_type="tiered",
    tier_rules=[
        TierRule(min_revenue=Decimal("0"),     max_revenue=Decimal("10000"), platform_share=Decimal("30"), owner_share=Decimal("70")),
        TierRule(min_revenue=Decimal("10000"), max_revenue=Decimal("50000"), platform_share=Decimal("20"), owner_share=Decimal("80")),
        TierRule(min_revenue=Decimal("50000"), max_revenue=None,              platform_share=Decimal("15"), owner_share=Decimal("85")),
    ],
    currency="SEK",
    valid_from=date(2026, 1, 1),
    valid_to=None,
    vat_rate=Decimal("25"),
    split_on_net=True,
)

Important: the matched tier's rate is applied to the entire basis, not to the portion of the basis that falls in the tier. A 60000 SEK payment gets the 15% rate on all 60000, not 30%/20%/15% on the sliced portions. If you need progressive tiered pricing, flatten the tiers manually or build a custom rule type — the built-in tiered type is the flat-rate variety, which is much more common in practice.

Date-ranged rules in practice

Create a new rule with a valid_from in the future to schedule a rate change:

# Current rule runs until April 30
old = RevenueSplitRule(
    ...,
    valid_from=date(2026, 1, 1),
    valid_to=date(2026, 5, 1),
    platform_share=Decimal("30"),
)

# New rule takes effect May 1
new = RevenueSplitRule(
    ...,
    valid_from=date(2026, 5, 1),
    valid_to=None,
    platform_share=Decimal("25"),
)

When the settlement runner processes April payments on May 2, get_active_rule(tenant_id, as_of=payment.paid_at) returns the old rule — because the payment was made in April. The new rule applies to payments made on or after May 1.

This is the single most important feature of the revenue split system. Retroactive rate changes are common mistakes; the date-range guard makes them impossible.

VAT and split_on_net

Swedish accounting treats VAT as pass-through money — the tenant collects it on behalf of the tax authority and does not own it. Split calculations should therefore happen on the net amount, which is why split_on_net defaults to True.

Set split_on_net=False only if your commercial agreement explicitly says the split is over gross. That is unusual — it means the platform takes a cut of the VAT, which the tenant then has to remit to the tax authority out of its own pocket. Make sure the agreement is clear before flipping this flag.

Read Tenant Agreements for the document that holds the rules alongside service fees and settlement order, and Settlements & Payouts for how split records become actual payouts.