Skip to content

Client Funds

A client funds account holds money that belongs to someone else. Lawyers hold client funds when a settlement is paid before distribution. Property managers hold client funds as rent deposits. Platform marketplaces hold client funds between the buyer's payment and the seller's payout. In every case, the money must be segregated from the platform's own assets — it is not platform revenue, it is custodial money that needs its own paper trail and, in many jurisdictions, its own bank account.

Craft Easy's client-fund subsystem provides the data model and the transaction log. It does not set up bank accounts for you (that is a regulatory question), but it gives you the internal ledger that proves, to the krona, what each tenant is holding on whose behalf.

When to use client funds

Use client funds when your answer to "whose money is this?" is not "the platform's" and not "the tenant's own operating revenue". If a regulator, an auditor, or a court could ask you to demonstrate the exact balance of custodial money held at a point in time, you need client funds.

Do not use client funds for:

  • The platform's own operating revenue → that is the regular bookkeeping ledger (1930 Bank + 3000 Sales).
  • Money in transit from a payment provider to the tenant → that is a regular Payment with status="processing".
  • Prepayments or deposits that legally become the tenant's property on receipt → those are ordinary liabilities on the tenant's books, not custodial funds.

Client funds are specifically for money the tenant is legally obliged to keep separate from their own assets.

The two models

ClientFundAccount

craft_easy.models.client_fund.ClientFundAccount is the container. One account per tenant per currency.

Field Purpose
tenant_id Who holds the funds
balance Current balance, Decimal
currency ISO 4217
is_frozen When true, withdrawals are blocked. Deposits still succeed

A tenant can have multiple accounts if they accept multiple currencies — one SEK account, one EUR account, one NOK account — but only one per currency, so the balance per currency is unambiguous.

The is_frozen flag is for compliance holds. If a regulator subpoenas a tenant's client funds, staff freeze the account; deposits continue (money keeps flowing in) but no withdrawals leave until the hold is cleared. Frozen accounts surface in health checks so the operations team knows they exist.

ClientFundTransaction

craft_easy.models.client_fund.ClientFundTransaction is the immutable log. Every change to an account's balance is recorded as a transaction, and transactions are never deleted or amended.

Field Purpose
account_id Link to the ClientFundAccount
transaction_type deposit, withdrawal, fee, interest, adjustment
amount Signed: positive for deposits, negative for withdrawals. The service enforces the sign per type
balance_after The account balance after this transaction was applied
currency ISO 4217, matches the account's currency
description Free-form description
reference_type, reference_id Link back to the business event — a payment that funded the deposit, a payout that drained the withdrawal, a settlement
timestamp When the transaction occurred

The record has use_revision=False on its config — there is no way to edit a client-fund transaction once it has been written. Corrections happen through a new reversing transaction with transaction_type="adjustment" and a description that references the original.

Transaction types

Type Sign Purpose
deposit positive Money arrives into the account (customer payment, inbound transfer)
withdrawal negative Money leaves the account (payout to the beneficiary, refund to the customer)
fee negative Management fee deducted from the held funds (only if the agreement authorizes it)
interest positive or negative Interest credited to or debited from the account
adjustment either Manual correction; requires a human description and, by policy, usually a dual approval

The balance_after field is stored on every transaction for reasons that matter at audit time: it lets you reconstruct the balance at any historical moment by reading one row, without replaying every transaction. It also makes it trivial to detect tampering — if the log has been edited, the balance_after chain breaks, and every subsequent transaction fails a consistency check.

Reading the log

The routes are plain CRUD on the two models:

GET  /client-fund-accounts                       # list accounts (filter: tenant_id, currency, is_frozen)
GET  /client-fund-accounts/{account_id}          # fetch an account
POST /client-fund-accounts                       # create (usually one per tenant per currency)
PATCH /client-fund-accounts/{account_id}         # update (is_frozen, typically)

GET  /client-fund-transactions                   # list transactions (filter: account_id, type, date)
GET  /client-fund-transactions/{transaction_id}  # fetch one
POST /client-fund-transactions                   # record a new transaction (service-backed)

The POST /client-fund-transactions endpoint validates the sign of the amount against the transaction type, updates the account's balance, computes balance_after, and writes both documents in a single atomic operation. Application code should never write to ClientFundAccount.balance directly — always go through the transaction endpoint so the log and the balance stay in sync.

A worked example — rent deposit

A property manager uses the platform to collect rent deposits from tenants. The platform holds the deposit until the lease ends, then releases it (minus any withheld amounts) back to the tenant.

# 1. Create the account (once, on property-manager onboarding)
account = await ClientFundAccount(
    tenant_id=property_manager.id,
    balance=Decimal("0"),
    currency="SEK",
).insert()

# 2. A renter pays a 12,000 SEK deposit
await client_fund_svc.record_transaction(
    account_id=account.id,
    transaction_type="deposit",
    amount=Decimal("12000"),
    description="Deposit for apartment #42B, lease 2026-2028",
    reference_type="payment",
    reference_id=payment.id,
)
# account.balance == 12000

# 3. Two years later, lease ends. Property manager withholds 500 SEK for cleaning.
await client_fund_svc.record_transaction(
    account_id=account.id,
    transaction_type="withdrawal",
    amount=Decimal("-500"),
    description="Cleaning fee deducted from deposit for apartment #42B",
    reference_type="invoice",
    reference_id=cleaning_invoice.id,
)
# account.balance == 11500

# 4. Return the remainder to the renter
await client_fund_svc.record_transaction(
    account_id=account.id,
    transaction_type="withdrawal",
    amount=Decimal("-11500"),
    description="Deposit return for apartment #42B, lease end",
    reference_type="payout",
    reference_id=payout.id,
)
# account.balance == 0

Four rows in the transaction log. Balance goes from 0 to 12000 to 11500 to 0. Every step has a reference_type/reference_id pair pointing back to the business object. If anyone asks "what happened to the deposit?", the answer is four SQL-ish queries, and the answer is complete.

Interaction with bookkeeping

If the main bookkeeping ledger is enabled, client-fund movements should also post double-entry transactions against a dedicated client-funds account (1950 Client funds account in the BAS default chart).

A deposit:

Account Debit Credit
1950 Client funds 12000
2499 Client fund liability (offsetting liability) 12000

A withdrawal to the beneficiary:

Account Debit Credit
2499 Client fund liability 12000
1950 Client funds 12000

The client-fund subsystem does not post these ledger entries itself — it is a separate ledger with its own schema. Wire up the posting in a hook on ClientFundTransaction or in the application-level service that records the transaction. Keeping the two ledgers separate lets you audit client funds without having to reason about the rest of the books, which matters when the regulator asks for exactly that.

Operational notes

  • Separate bank accounts. The data model tracks the money, but the money itself still needs a real bank account somewhere. Most jurisdictions require a segregated bank account for client funds — one per currency, with the account name registered with the bank as a trust account.
  • Reconciliation. Run a periodic reconciliation that compares sum(ClientFundAccount.balance for currency=X) against the bank balance for the segregated account in that currency. Any drift is an incident — investigate immediately, do not handwave it as "rounding".
  • Interest. If the bank pays interest on the segregated account, credit it to the accounts proportionally using transaction_type="interest". Whose interest it is (the tenant's, the platform's, the beneficiary's) is a legal question — answer it once in the service agreement, then encode the answer in the posting logic.
  • Auditors will ask for the full transaction log. Provide it by date range with a single query; do not make them grep through logs. The GET /client-fund-transactions endpoint with ?created_at__gte=&created_at__lt= filters is the one to use.

Read Bookkeeping for the main double-entry ledger that sits next to client funds, and Settlements & Payouts for how tenant-owned money (not custodial money) flows through the platform.