Open raw ↗# -*- coding: utf-8 -*-
"""
model_params.py — C6 FINANCIAL MODEL.
Every input below carries the SRC-### or ASM-### it comes from. Nothing is hard-coded
without a reference. Serves REQ-SYS-05, REQ-SYS-07, REQ-SYS-10, REQ-SYS-11.
Run directly to print the computed result set.
"""
# =========================================================================
# BLOCK 1 — SOURCED RATE INPUTS
# =========================================================================
INPUTS = {
# key: (value, unit, source_id, confidence)
"price_selfcare_wd": (73.58, "AUD/hr", "SRC-002", "Medium"),
"price_community_wd": (73.58, "AUD/hr", "SRC-004", "Medium"),
"price_supportcoord_l2": (100.14, "AUD/hr", "SRC-005", "Medium"),
"wage_sacs_l2_casual": (45.28, "AUD/hr", "SRC-009", "Medium"),
"wage_sacs_l3_casual": (50.61, "AUD/hr", "SRC-009", "Medium"),
"oncost_super": (0.1200, "rate", "SRC-011", "High"),
"oncost_workcover": (0.0180, "rate", "SRC-012", "Medium"),
"oncost_portable_lsl": (0.0165, "rate", "SRC-013", "High"),
"oncost_payroll_tax": (0.0000, "rate", "ASM-003", "Medium"), # excluded below threshold
}
# =========================================================================
# BLOCK 2 — ASSUMED DRIVERS (change these; everything downstream recomputes)
# =========================================================================
DRIVERS = {
"admin_hours_per_billable_hour": (0.25, "hr", "ASM-008", "Low"),
"payment_lag_days": (14, "days", "ASM-007", "Low"),
"bookkeeping_per_month": (300, "AUD/mo", "ASM-005", "Low"),
"general_overhead_per_month": (200, "AUD/mo", "ASM-014", "Low"),
"website_one_off": (2500, "AUD", "ASM-006", "Low"),
"sc_utilisation": (0.55, "rate", "ASM-013", "Low"),
"sc_hours_available_per_week": (38, "hr", "ASM-011", "Low"),
"first_client_month": (4, "month", "ASM-015", "Low"),
}
# =========================================================================
# BLOCK 3 — SOURCED ONE-OFF AND RECURRING COSTS
# =========================================================================
# (label, low, base, high, source_id, confidence)
ONE_OFF_COSTS = [
("ASIC Pty Ltd company registration", 636, 636, 636, "SRC-025", "Medium"),
("ASIC business name, three years", 108, 108, 108, "SRC-027", "Medium"),
("ABN, TFN, GST and PAYG registration", 0, 0, 0, "SRC-028", "High"),
("myID and RAM setup", 0, 0, 0, "SRC-030", "High"),
("NDIS Worker Screening Check, per person", 139.20,139.20,139.20,"SRC-031", "High"),
("First aid HLTAID011 plus CPR HLTAID009", 290, 290, 290, "SRC-041", "Medium"),
("NDIS Worker Orientation Module", 0, 0, 0, "SRC-042", "Medium"),
("Website and brand identity", 2500, 2500, 2500, "ASM-006", "Low"),
("Policy and procedure manual, verification pack", 649, 1080, 1825, "SRC-036", "High"),
("Verification audit fee", 3500, 4750, 6000, "SRC-034", "Medium"),
]
RECURRING_MONTHLY = [
("CRM and rostering (ShiftCare Essentials)", 45, 45, 125, "SRC-037", "High"),
("Accounting and payroll (Xero)", 78, 78, 143, "SRC-038", "High"),
("Bookkeeping", 200, 300, 500, "ASM-005", "Low"),
("General operating overhead", 150, 200, 350, "ASM-014", "Low"),
]
RECURRING_ANNUAL = [
("ASIC company annual review", 342, 342, 342, "SRC-026", "Medium"),
("Insurance: public liability, professional indemnity, personal accident",
1200, 2350, 3500, "SRC-040", "Low"),
]
WORKERS_AT_START = 3 # support workers screened and trained before first shift
CERT_AUDIT = (6000, 9000, 12000, "SRC-034") # certification pathway, for reference only
# =========================================================================
# BLOCK 4 — COMPUTATION
# =========================================================================
def v(d, k):
return d[k][0]
def oncost_multiplier():
"""Fully-loaded multiplier applied to the casual hourly rate."""
return (1.0
+ v(INPUTS, "oncost_super")
+ v(INPUTS, "oncost_workcover")
+ v(INPUTS, "oncost_portable_lsl")
+ v(INPUTS, "oncost_payroll_tax"))
def loaded_wage(level="l2"):
base = v(INPUTS, "wage_sacs_l2_casual") if level == "l2" else v(INPUTS, "wage_sacs_l3_casual")
return base * oncost_multiplier()
def gross_margin_per_hour():
"""REQ-SYS-05. Price minus fully-loaded direct labour. BEFORE administration."""
return v(INPUTS, "price_selfcare_wd") - loaded_wage("l2")
def contribution_per_hour(admin_paid: bool):
"""
Contribution after the administration burden each billable hour creates.
admin_paid=False : the owner does the administration unpaid (capacity-limited, not free).
admin_paid=True : administration is done by a paid coordinator at SACS Level 3 loaded.
"""
gm = gross_margin_per_hour()
if not admin_paid:
return gm
return gm - (loaded_wage("l3") * v(DRIVERS, "admin_hours_per_billable_hour"))
def band(rows, idx):
return sum(r[idx] for r in rows)
def one_off_total(workers=WORKERS_AT_START, include_registration=True):
"""Low, base, high one-off capital. Per-person items scale by (owner + workers)."""
people = workers + 1
out = []
for i in (1, 2, 3):
t = 0.0
for label, lo, ba, hi, src, conf in ONE_OFF_COSTS:
val = (lo, ba, hi)[i - 1]
if "per person" in label or "First aid" in label or "Orientation" in label:
val *= people
if not include_registration and ("audit" in label.lower() or "Policy and procedure" in label):
val = 0
t += val
out.append(round(t, 2))
return tuple(out)
def monthly_fixed():
ann = [band(RECURRING_ANNUAL, i) / 12.0 for i in (1, 2, 3)]
mon = [band(RECURRING_MONTHLY, i) for i in (1, 2, 3)]
return tuple(round(m + a, 2) for m, a in zip(mon, ann))
def breakeven_hours(admin_paid: bool, which=2):
"""REQ-SYS-10. Billable hours per month at which contribution equals fixed cost."""
return monthly_fixed()[which - 1] / contribution_per_hour(admin_paid)
def working_capital(billable_hours_per_month):
"""REQ-SYS-11. Cash needed to fund wages before payment arrives."""
wages = billable_hours_per_month * loaded_wage("l2")
return wages * (v(DRIVERS, "payment_lag_days") / 30.0)
# ---- Ramp and cash curve -------------------------------------------------
RAMP_BASE = [0, 0, 0, 40, 80, 130, 180, 230, 280, 320, 350, 380] # ASM-015
RAMP_SLOW = [0, 0, 0, 0, 0, 0, 30, 70, 110, 150, 190, 230] # first client month 7
RAMP_ZERO = [0] * 12 # REQ-SYS-07 downside
def cash_curve(ramp, admin_paid=False, owner_draw=0.0, which=2, include_registration=True):
"""Month-by-month cash position. Returns (rows, cumulative_min, month_cash_positive)."""
one_off = one_off_total(include_registration=include_registration)[which - 1]
fixed = monthly_fixed()[which - 1]
cpu = contribution_per_hour(admin_paid)
rows, cum, worst, first_pos = [], -one_off, -one_off, None
for m, hrs in enumerate(ramp, start=1):
rev = hrs * v(INPUTS, "price_selfcare_wd")
wages = hrs * loaded_wage("l2")
admin = hrs * loaded_wage("l3") * v(DRIVERS, "admin_hours_per_billable_hour") if admin_paid else 0.0
contrib = hrs * cpu
net = contrib - fixed - owner_draw
cum += net
worst = min(worst, cum)
if first_pos is None and cum > 0:
first_pos = m
rows.append({
"month": m, "billable_hours": hrs, "revenue": round(rev, 2),
"direct_wages": round(wages, 2), "paid_admin": round(admin, 2),
"contribution": round(contrib, 2), "fixed_costs": round(fixed, 2),
"owner_draw": round(owner_draw, 2), "net_cash": round(net, 2),
"cumulative_cash": round(cum, 2),
})
return rows, round(worst, 2), first_pos
MANAGER_SALARY_PA = 95000.0 # ASM-011: a passive-investor model must employ a manager
def breakeven_hours_employed_manager(which=2):
"""REQ-SYS-10, ASM-011. Break-even when the owner does NOT work in the business and a
manager is employed on a fixed salary. This is a DIFFERENT scenario from paid administration
(which scales with volume) and must not be confused with it."""
fixed = monthly_fixed()[which - 1] + MANAGER_SALARY_PA / 12.0
return fixed / contribution_per_hour(False)
def runway_downside(months=6, owner_draw=0.0, which=2, include_registration=True, workers=None):
"""REQ-SYS-07. Cash needed if no participant is onboarded for `months`."""
if workers is None:
workers = WORKERS_AT_START
one_off = one_off_total(workers=workers, include_registration=include_registration)[which - 1]
fixed = monthly_fixed()[which - 1]
return round(one_off + (fixed + owner_draw) * months, 2)
# ---- Support coordination branch ----------------------------------------
def sc_annual():
"""Owner-delivered support coordination. No direct wage cost; the constraint is utilisation."""
hrs_wk = v(DRIVERS, "sc_hours_available_per_week") * v(DRIVERS, "sc_utilisation")
hrs_mo = hrs_wk * 52.0 / 12.0
rev_mo = hrs_mo * v(INPUTS, "price_supportcoord_l2")
fixed = monthly_fixed()[1]
return {
"billable_hours_per_week": round(hrs_wk, 1),
"billable_hours_per_month": round(hrs_mo, 1),
"revenue_per_month": round(rev_mo, 2),
"fixed_per_month": round(fixed, 2),
"net_per_month": round(rev_mo - fixed, 2),
"net_per_year": round((rev_mo - fixed) * 12, 2),
"breakeven_hours_per_month": round(fixed / v(INPUTS, "price_supportcoord_l2"), 1),
}
def sc_sensitivity():
out = []
fixed = monthly_fixed()[1]
for u in (0.25, 0.35, 0.45, 0.55, 0.65, 0.75):
hrs_mo = v(DRIVERS, "sc_hours_available_per_week") * u * 52.0 / 12.0
rev = hrs_mo * v(INPUTS, "price_supportcoord_l2")
out.append((u, round(hrs_mo, 1), round(rev, 2), round((rev - fixed) * 12, 2)))
return out
def margin_sensitivity():
"""What each uncertain input does to gross margin per billable hour."""
gm = gross_margin_per_hour()
price = v(INPUTS, "price_selfcare_wd")
out = [("Base case, all inputs as sourced", round(gm, 2), "")]
# ASM-002: Level 3 instead of Level 2
gm_l3 = price - loaded_wage("l3")
out.append(("ASM-002 wrong: workers sit at SACS Level 3, not Level 2", round(gm_l3, 2),
"Margin falls %.0f%%" % ((gm - gm_l3) / gm * 100)))
# ASM-004: WorkCover at 3.0% instead of 1.8%
m = 1 + 0.12 + 0.030 + 0.0165
gm_wc = price - v(INPUTS, "wage_sacs_l2_casual") * m
out.append(("ASM-004 wrong: WorkCover industry rate is 3.0%, not the 1.8% state average",
round(gm_wc, 2), "Margin falls %.0f%%" % ((gm - gm_wc) / gm * 100)))
# ASM-003: payroll tax applies
m = 1 + 0.12 + 0.018 + 0.0165 + 0.0485
gm_pt = price - v(INPUTS, "wage_sacs_l2_casual") * m
out.append(("ASM-003 wrong: the business crosses the payroll tax threshold",
round(gm_pt, 2), "Margin falls %.0f%%" % ((gm - gm_pt) / gm * 100)))
# Paid admin at the assumed ratio
gm_ad = contribution_per_hour(True)
out.append(("Administration is paid rather than done by the owner (ASM-008 at 0.25 hr)",
round(gm_ad, 2), "Margin falls %.0f%%" % ((gm - gm_ad) / gm * 100)))
# Paid admin at a worse ratio
gm_ad2 = gm - loaded_wage("l3") * 0.40
out.append(("Administration is paid and runs at 0.40 hr per billable hour",
round(gm_ad2, 2), "Margin falls %.0f%%" % ((gm - gm_ad2) / gm * 100)))
return out
# =========================================================================
# BLOCK 5 — HEADLINE RESULT SET (imported by the study and deck builders)
# =========================================================================
def results():
oo_reg = one_off_total(include_registration=True)
oo_sc = one_off_total(workers=0, include_registration=False)
fx = monthly_fixed()
base_rows, base_worst, base_pos = cash_curve(RAMP_BASE, admin_paid=False)
slow_rows, slow_worst, slow_pos = cash_curve(RAMP_SLOW, admin_paid=False)
paid_rows, paid_worst, paid_pos = cash_curve(RAMP_BASE, admin_paid=True)
return {
"oncost_multiplier": round(oncost_multiplier(), 4),
"loaded_wage_l2": round(loaded_wage("l2"), 2),
"loaded_wage_l3": round(loaded_wage("l3"), 2),
"gross_margin_hr": round(gross_margin_per_hour(), 2),
"gross_margin_pct": round(gross_margin_per_hour() / v(INPUTS, "price_selfcare_wd") * 100, 1),
"contribution_owner_admin": round(contribution_per_hour(False), 2),
"contribution_paid_admin": round(contribution_per_hour(True), 2),
"one_off_core": oo_reg,
"one_off_sc": oo_sc,
"monthly_fixed": fx,
"breakeven_hrs_owner_admin": round(breakeven_hours(False), 1),
"breakeven_hrs_paid_admin": round(breakeven_hours(True), 1),
"breakeven_hrs_employed_manager": round(breakeven_hours_employed_manager(), 1),
"manager_salary_pa": MANAGER_SALARY_PA,
"working_capital_at_300h": round(working_capital(300), 2),
"working_capital_at_300h_30day": round(300 * loaded_wage("l2"), 2),
"runway_6mo_core_no_draw": runway_downside(6, 0.0),
"runway_6mo_core_draw6k": runway_downside(6, 6000.0),
"runway_6mo_sc_no_draw": runway_downside(6, 0.0, include_registration=False, workers=0),
"base_rows": base_rows, "base_worst": base_worst, "base_month_positive": base_pos,
"slow_rows": slow_rows, "slow_worst": slow_worst, "slow_month_positive": slow_pos,
"paid_rows": paid_rows, "paid_worst": paid_worst, "paid_month_positive": paid_pos,
"sc": sc_annual(), "sc_sens": sc_sensitivity(), "margin_sens": margin_sensitivity(),
"hours_for_60k_draw": round((monthly_fixed()[1] + 5000) / contribution_per_hour(False), 0),
"hours_for_60k_draw_paid_admin": round((monthly_fixed()[1] + 5000) / contribution_per_hour(True), 0),
}
if __name__ == "__main__":
import json
r = results()
for k, val in r.items():
if k.endswith("_rows"):
print(k, "-> %d months, final cumulative %s" % (len(val), val[-1]["cumulative_cash"]))
else:
print(k, "=", val)