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:
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:
- 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. - Net revenue:
net_revenue = gross_revenue - vat_amount. - Split basis:
basis = net_revenue if rule.split_on_net else gross_revenue. - Split computation per
split_type: percentage:platform = basis * platform_share / 100,owner = basis * owner_share / 100.fixed:platform = min(rule.platform_fixed, basis),owner = basis - platform. The min clamp prevents a fixed fee from exceeding the basis on tiny payments.tiered: walkstier_ruleslooking for the tier wheremin_revenue <= basis < max_revenue(ormax_revenue is None). Applies that tier'splatform_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.