Investment Plans workspace
Open raw ↗
# -*- coding: utf-8 -*-
"""
build_business_plan_json.py — derives 01_System/business_plan.json from the financial models.

OWNER: 01_System/build_business_plan_json.py
REVIEW: whenever model_params.py or model_agedcare.py changes, or the Standard's PIL-BZ
        schema changes.

WHY THIS EXISTS
---------------
REQ-BUS-001 (Standard v4.0) requires a business_plan.json twin. GOV-B4 requires that no
figure is hand-typed twice. So the twin is a PROJECTION of the two financial models, not a
document someone wrote. Change a wage rate in model_params.py and every number in the
business plan and its A3 page moves with it.

The narrative text is authored here; every NUMBER is computed. Where a number rests on an
assumption rather than a source, the assumption ID is named in the text beside it.
"""
import os, sys, json, datetime

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import model_params as N
import model_agedcare as AC
import project_data as P

LIFECYCLE_YEARS = 3
OUT = os.path.join(HERE, "business_plan.json")

# ---------------------------------------------------------------- ramp assumptions
# NDIS: the base-case ramp already modelled and verified in model_params.results().
NDIS_HOURS_Y1 = [r["billable_hours"] for r in N.results()["base_rows"]]
NDIS_HOURS_STEADY = NDIS_HOURS_Y1[-1]                      # month 12 rate, held flat
# Aged care: client ramp, ASM-024 target of 12 clients reached at month 12, held flat.
AC_CLIENTS_Y1 = [0, 0, 0, 1, 2, 3, 4, 6, 8, 10, 11, 12]
AC_CLIENTS_STEADY = AC_CLIENTS_Y1[-1]
OWNER_FOREGONE_INCOME_PA = 60000.0                          # ASM-028
# Maintenance over the lifecycle (ASM-029): audit and recertification, both entities.
MAINTENANCE = [
    ("NDIS verification audit, mid-term", 2200.0, "ASM-029"),
    ("Aged Care Quality and Safety Commission audit and registration renewal", 3000.0, "ASM-026"),
    ("First aid and CPR recertification, three-year cycle, eight people", 8 * 290.0, "SRC-041"),
]


def money(x):
    return round(float(x), 2)


def _verified_by():
    """DEF-052. This said 'PENDING - pass V5 has not yet run' through passes V5 to V10, all of
    which ran. A provenance field that names a pass by number goes stale the moment the next one
    starts, so it now names the pass that actually set the current requirement statuses and reads
    the count out of the register rather than restating it."""
    import project_data as _P
    n_pass = len(_P.iv_verified())
    return ("%s — %d of %d requirements verified against baseline %s, %d still open"
            % (_P.IV_VERDICT_SOURCE.split("(")[0].strip().rstrip(","),
               n_pass, len(_P.REQ), _P.BASELINE_VERSION, len(_P.REQ) - n_pass))


def build():
    R = AC.results()
    NR = N.results()
    S = R["structure"]

    # DEF-033. The published $6.70 is rounded; using it inflated three-year benefits by $34.12.
    # Immaterial to the decision, material to whether the number reconciles when someone checks.
    ndis_contrib_hr_paid = (N.INPUTS["price_selfcare_wd"][0] - N.loaded_wage("l2")
                            - N.v(N.DRIVERS, "admin_hours_per_billable_hour") * N.loaded_wage("l3"))
    ndis_contrib_hr_owner = NR["contribution_owner_admin"]      # 21.30
    ac_contrib_client_paid = R["ac_client_paid_admin"]["contribution_per_client_month"]
    ac_contrib_client_owner = R["ac_client_owner_admin"]["contribution_per_client_month"]

    # ---- monthly contribution series across the lifecycle, paid-administration basis
    months = []
    for y in range(LIFECYCLE_YEARS):
        for i in range(12):
            hrs = NDIS_HOURS_Y1[i] if y == 0 else NDIS_HOURS_STEADY
            cl = AC_CLIENTS_Y1[i] if y == 0 else AC_CLIENTS_STEADY
            months.append({
                "month": y * 12 + i + 1,
                "ndis_hours": hrs,
                "ac_clients": cl,
                "ndis_contribution": hrs * ndis_contrib_hr_paid,
                "ac_contribution": cl * ac_contrib_client_paid,
                "fixed": S["two_entities_fixed_month"],
            })

    dev = S["two_entities_oneoff"]
    running_total = S["two_entities_fixed_month"] * 12 * LIFECYCLE_YEARS
    maintenance = sum(m[1] for m in MAINTENANCE)
    opportunity = OWNER_FOREGONE_INCOME_PA * LIFECYCLE_YEARS
    quant_benefit = sum(m["ndis_contribution"] + m["ac_contribution"] for m in months)
    total_cost = dev + running_total + maintenance + opportunity
    net = quant_benefit - total_cost
    roi = net / total_cost * 100.0

    # ---- break-even: cumulative cash, one-off spent at month 0, opportunity cost carried
    cum = -dev
    be_month = None
    monthly_opp = OWNER_FOREGONE_INCOME_PA / 12.0
    for m in months:
        cum += m["ndis_contribution"] + m["ac_contribution"] - m["fixed"] - monthly_opp
        if cum >= 0 and be_month is None:
            be_month = m["month"]
    # ---- break-even ignoring the owner's foregone income (cash break-even)
    cum2, be_cash = -dev, None
    for m in months:
        cum2 += m["ndis_contribution"] + m["ac_contribution"] - m["fixed"]
        if cum2 >= 0 and be_cash is None:
            be_cash = m["month"]

    y1_rev_ndis = sum(NDIS_HOURS_Y1) * N.INPUTS["price_selfcare_wd"][0]
    y_steady_rev_ndis = NDIS_HOURS_STEADY * 12 * N.INPUTS["price_selfcare_wd"][0]
    ac_month_rev = AC.v(AC.DRIVERS, "avg_client_budget") / 12.0
    sc_rev = NR["sc"]["revenue_per_month"] * 12.0
    y1_rev_ac = sum(AC_CLIENTS_Y1) * ac_month_rev
    y_steady_rev_ac = AC_CLIENTS_STEADY * 12 * ac_month_rev

    # ---- running-cost categories, both entities, per lifecycle year
    def cat(label, monthly_both, source):
        return {"category": label, "annual": [money(monthly_both * 12)] * LIFECYCLE_YEARS,
                "source": source}

    running_costs = [
        cat("Accounting and payroll, two entities", 78 * 2, "SRC-038"),
        cat("Bookkeeping, two entities", 300 * 2, "ASM-005"),
        cat("Rostering, claiming and care-management software, two entities",
            N.RECURRING_MONTHLY[0][2] + AC.RECURRING_MONTHLY[0][2], "ASM-027"),
        cat("General operating overhead, two entities", 200 * 2, "ASM-014"),
        {"category": "Insurance: public liability, professional indemnity, management liability",
         "annual": [money(N.band(N.RECURRING_ANNUAL, 2) + AC.band(AC.RECURRING_ANNUAL, 2)
                          - 342 * 2)] * LIFECYCLE_YEARS, "source": "SRC-067"},
        {"category": "ASIC company annual review, two entities",
         "annual": [money(342 * 2)] * LIFECYCLE_YEARS, "source": "SRC-026"},
    ]

    d = {
      "meta": {
        "project_name": "Victorian NDIS and Aged Care Venture Portfolio",
        "owner": "Zaid Aldabbag",
        "date": datetime.date.today().isoformat(),
        "status": "BUILT, UNVERIFIED",
        "class": P.PROJECT_CLASS,
        "version": "2.0.0-draft",
        "standard": "AI Project Governance Standard v4.0 (Business Layer) over v3.9"
      },

      "purpose": {
        "statement": (
          "Decide whether to start two Victorian care businesses — a registered NDIS provider and "
          "a Support at Home aged care provider, held as two separate companies — and if so, in "
          "which order and on what capital. The decision must rest on verified prices, awarded "
          "wages and named assumptions, not on the sector's headline growth figures."),
        "problem": (
          "Both sectors publish large market numbers and neither publishes the number that "
          "actually decides viability: what one client contributes after the labour and the "
          "administration that client generates. Roughly half of NDIS providers were loss-making "
          "in 2024-25 while the sector grew, which is only explicable once administration is "
          "priced into the margin rather than assumed away."),
        "current_state": (
          "No entity registered, no workers engaged, no clients. Capital is unallocated and the "
          "owner's time is the binding scarce resource."),
        "desired_state": (
          "A go or no-go made on a per-client contribution figure that survives independent "
          "verification, with the capital requirement, the working-capital gap and the "
          "administration break-point all quantified before any money is committed."),
        "gap": (
          "The prior material held no working-capital figure, no administration load in the "
          "margin, and no comparison unit that made the two sectors comparable. This project "
          "closes that gap by modelling both per client per month.")
      },

      "target_market": [
        {"segment": "NDIS participants in Victoria using self-managed or plan-managed core supports",
         "size": "Active NDIS participants nationally, per the registered participant count; Victoria is the second largest state cohort",
         "size_source": "SRC-043, SRC-044",
         "need": "Reliable, rostered daily personal care and community access at the published price limit",
         "why_us": "A new entrant with no legacy overhead can operate below the sector's average cost base, which is where the sector's losses originate"},
        {"segment": "Support at Home participants in Victoria holding an assessed classification budget",
         "size": "The national in-home aged care cohort moving onto Support at Home classifications, per the programme data report and the older-Australians demographic profile",
         "size_source": "SRC-070, SRC-073",
         "need": "Delivery inside a fixed annual budget, with the care-management pool actively managed rather than absorbed",
         "why_us": "The mandatory ten per cent care-management pool is revenue the NDIS has no equivalent of, and a small provider can service it with the owner's own time in year one"},
        {"segment": "NDIS support coordination and plan management",
         "size": "A capability line rather than a client cohort; served from the same participant base",
         "size_source": "ASM-024",
         "need": "Coordination hours that require no support workforce and no working capital against wages",
         "why_us": "It is the only line in either business that produces a positive contribution from the owner's time alone, with no wage exposure"}
      ],

      "value_proposition": {
        "statement": (
          "Two separate entities sharing one operating discipline: a per-client contribution "
          "figure computed before the client is accepted, and an administration load that is "
          "priced rather than absorbed. The aged care entity funds the group; the NDIS entity "
          "buys the workforce and the referral relationships."),
        "benefits": [
          {"benefit": "Aged care contributes materially more per client than NDIS on a third of the service hours",
           "metric": "Contribution per client per month, paid-administration basis",
           "baseline": "NDIS $%s a client a month, on %s billable hours a week"
                       % (f"{R['ndis_client_paid_admin']['contribution_per_client_month']:,.2f}",
                          R["ndis_client_paid_admin"]["billable_hours_per_week"]),
           "target": "$%s aged care against $%s NDIS, a ratio of %s to one" % (
               f"{ac_contrib_client_paid:,.2f}",
               f"{R['ndis_client_paid_admin']['contribution_per_client_month']:,.2f}",
               R["contribution_ratio_paid_admin"]),
           "measured_by": "model_agedcare.client_economics() against model_agedcare.ndis_client_economics()"},
          {"benefit": "The administration break-point is known before hiring, not discovered afterwards",
           "metric": "NDIS contribution per billable hour as administration load rises",
           "baseline": "$%s per hour with the owner doing administration" % f"{ndis_contrib_hr_owner:,.2f}",
           "target": "Stay below 0.31 administration hours per billable hour; at 0.40 the contribution is negative $2.07",
           "measured_by": "model_params.margin_sensitivity(), ASM-008"},
          {"benefit": "The capital requirement is stated as a number rather than a range of opinion",
           "metric": "Six-month downside runway plus working capital, both entities",
           "baseline": "No figure existed in the prior material",
           "target": "$%s combined six-month runway with no owner draw, plus $%s working capital" % (
               f"{NR['runway_6mo_core_no_draw'] + R['runway_6mo_no_draw']:,.2f}",
               f"{NR['working_capital_at_300h'] + R['working_capital']:,.2f}"),
           "measured_by": "model_params.runway_downside() and model_agedcare.runway_downside()"},
          {"benefit": "The cost of holding two entities rather than one is visible before the choice is locked",
           "metric": "Incremental fixed cost of separation",
           "baseline": "$%s per month for one combined entity" % f"{S['one_entity_fixed_month']:,.2f}",
           "target": "$%s per month, $%s per year, $%s over three years, accepted knowingly for liability separation" % (
               f"{S['monthly_cost_of_separation']:,.2f}",
               f"{S['annual_cost_of_separation']:,.2f}",
               f"{S['three_year_cost_of_separation']:,.2f}"),
           "measured_by": "model_agedcare.structure_comparison()"}
        ],
        "beneficiaries": [
          "Zaid Aldabbag as owner and sole capital provider",
          "Victorian NDIS participants and Support at Home clients receiving the service",
          "Support workers engaged under SCHADS with correctly loaded on-costs"
        ],
        "realisation_timeline": (
          "Aged care contribution begins in month four and reaches the twelve-client target at "
          "month twelve. NDIS cash turns positive in month %s on the base ramp and not at all "
          "within twelve months if administration is paid from the start." % NR["base_month_positive"])
      },

      "cba": {
        "lifecycle_years": LIFECYCLE_YEARS,
        "development_costs": money(dev),
        "running_costs_total": money(running_total),
        "maintenance_costs": money(maintenance),
        "opportunity_costs": money(opportunity),
        "quantitative_benefits": money(quant_benefit),
        "qualitative_benefits": [
          "Liability separation: a compliance failure in one entity does not reach the assets of the other",
          "Two independent regulatory registrations, so a suspension in one sector does not stop all revenue",
          "A verified financial model that can be re-run when a price limit or an award rate changes, rather than rebuilt",
          "Workforce transferable between entities under two employers, which raises retention against a sector-wide shortage"
        ],
        "net_benefit": money(net),
        "roi_pct": round(roi, 1),
        "break_even": (
          "Month %s counting the owner's foregone income of $%s a year as a cost; month %s on cash alone."
          % (be_month if be_month else "not within the lifecycle",
             f"{OWNER_FOREGONE_INCOME_PA:,.0f}",
             be_cash if be_cash else "not within the lifecycle")),
        "basis": (
          "Three-year lifecycle, paid-administration basis throughout, which is the conservative "
          "case: it assumes the owner never does unpaid administration. Benefits are contribution "
          "after direct labour and administration, before tax and before any owner draw. NDIS "
          "volume follows the verified base-case ramp; aged care follows the ASM-024 ramp to "
          "twelve clients. No growth is assumed in years two and three. The support coordination "
          "line is EXCLUDED from these benefits although the recommendation tells the owner to run "
          "it, because it is billed against the owner's own hours and those hours are already "
          "charged as an opportunity cost of $%s a year; counting the revenue as well would count "
          "the same hours twice. Including it would add roughly $%s of gross revenue over three "
          "years." % (f"{OWNER_FOREGONE_INCOME_PA:,.0f}", f"{sc_rev * 2.5:,.0f}"))
      },

      "running_costs": running_costs,

      "marketing_strategy": {
        "target_audience": (
          "Two distinct referrers, not two client groups: NDIS support coordinators and plan "
          "managers for the disability entity, and My Aged Care assessors, hospital discharge "
          "planners and general practices for the aged care entity."),
        "go_to_market": (
          "Aged care first. It reaches contribution on fewer clients, carries a seven-day payment "
          "lag against the NDIS plan-manager lag, and its care-management pool pays for the "
          "owner's time directly. The NDIS entity registers in parallel but takes clients second, "
          "once the aged care line covers the combined fixed cost."),
        "pricing": (
          "Both entities charge at the published ceiling: the NDIS price limit of $%s an hour, "
          "and the IHACPA advisory rate of $%s an hour for personal care. Neither is discounted; "
          "in the NDIS the limit is a cap and discounting removes the entire administration "
          "allowance, and in aged care the advisory rate is not a cap but the client's fixed "
          "budget is, so a lower rate buys the client more hours rather than winning the client."
          % (f"{N.INPUTS['price_selfcare_wd'][0]:,.2f}", f"{AC.INPUTS['price_personal_care'][0]:,.2f}")),
        "positioning": (
          "A small provider that answers the phone and sends the same worker each visit, competing "
          "on continuity rather than scale. The sector's losses come from overhead carried by "
          "large legacy providers, so the position is credible rather than aspirational."),
        "channels": [
          "Direct relationships with NDIS support coordinators and plan managers",
          "My Aged Care provider listing and assessor relationships",
          "Hospital discharge planners and general practices in the chosen Victorian catchment",
          "The provider finder listings both regulators publish"
        ]
      },

      "revenue_model": [
        {"use_case": "NDIS entity — core supports, daytime weekday personal care",
         "unit": "billable hour at the NDIS price limit",
         "unit_revenue": N.INPUTS["price_selfcare_wd"][0],
         "year_projection": [money(y1_rev_ndis), money(y_steady_rev_ndis), money(y_steady_rev_ndis)],
         "assumptions": [
           "Volume follows the verified base-case ramp: no billable hours before month four, %s hours a month by month twelve (ASM-024)" % NDIS_HOURS_STEADY,
           "Charged at the price limit with no discount; the limit is a cap, not a target",
           "Contribution is $%s an hour with paid administration and $%s with the owner administering (ASM-008)" % (
               f"{ndis_contrib_hr_paid:,.2f}", f"{ndis_contrib_hr_owner:,.2f}"),
           "No growth assumed in years two and three"
         ]},
        {"use_case": "Aged care entity — Support at Home personal care and care management",
         "unit": "client per month against an assessed classification budget",
         "unit_revenue": money(ac_month_rev),
         "year_projection": [money(y1_rev_ac), money(y_steady_rev_ac), money(y_steady_rev_ac)],
         "assumptions": [
           "Average assessed budget of $%s a year (ASM-020), which buys %s service hours a week after the ten per cent care-management deduction" % (
               f"{AC.v(AC.DRIVERS,'avg_client_budget'):,.0f}", R["hours_per_client_week"]),
           "Client count ramps to the ASM-024 target of %s by month twelve and is held flat" % AC_CLIENTS_STEADY,
           "The ten per cent care-management pool is billed in full and costs %s hours of coordinator time a client a month (ASM-021)" % AC.v(AC.DRIVERS, "care_mgmt_hours_per_client_mo"),
           "Two per cent of the participant contribution share is allowed for as bad debt (ASM-023)"
         ]},
        {"use_case": "NDIS entity — support coordination",
         "unit": "coordination hour, owner-delivered, no support workforce",
         "unit_revenue": N.INPUTS["price_support_coord"][0] if "price_support_coord" in N.INPUTS else 100.14,
         "year_projection": [money(sc_rev * 0.5), money(sc_rev), money(sc_rev)],
         "assumptions": [
           "GROSS revenue, stated on the same basis as the two rows above. V6 found this row previously "
           "stated NET of fixed cost while the others were gross, which made the three lines uncomparable",
           "NOT included in the cost-benefit above, deliberately: the CBA is the conservative case and this "
           "line depends entirely on the owner's own available hours, which are already charged against the "
           "venture as opportunity cost (ASM-028). Counting it as well would count the same hours twice",
           "Owner-delivered, so there is no wage exposure and no working capital against wages",
           "Break-even is %s hours a month against the entity's fixed cost" % NR["sc"]["breakeven_hours_per_month"],
           "Year one is taken at half the steady rate because registration and referral building consume the first six months",
           "Utilisation of 0.55 of available hours, which the sensitivity in the model brackets from 0.25 to 0.75"
         ]}
      ],

      "areas_of_concern": [
        {"concern": "Administration load pushes the NDIS contribution negative",
         "likelihood": "Likely", "impact": "Severe",
         "mitigation": "Keep administration with the owner until the NDIS line clears %s billable hours a month; the model states the break-point rather than leaving it to be discovered" % NR["breakeven_hrs_paid_admin"],
         "early_warning": "Administration hours per billable hour rising above 0.31 in the timesheet record",
         "register_id": "RSK-001"},
        {"concern": "Working capital is consumed by the payment lag before revenue arrives",
         "likelihood": "Likely", "impact": "Major",
         "mitigation": "Hold $%s against the NDIS lag at thirty days and $%s against the aged care lag at seven days, separately from the runway" % (
             f"{NR['working_capital_at_300h_30day']:,.2f}", f"{R['working_capital']:,.2f}"),
         "early_warning": "Days sales outstanding exceeding fourteen on plan-managed claims",
         "register_id": "RSK-002"},
        {"concern": "The assessed classification budget is lower than assumed, so each aged care client buys fewer hours",
         "likelihood": "Possible", "impact": "Major",
         "mitigation": "ASM-020 is Low confidence; the model brackets contribution from $%s a client a month at the BOTTOM ongoing classification of $%s to $%s at the top, and the break-even client count holds at one either way" % (
             f"{R['budget_sensitivity'][0][3]:,.2f}", f"{R['budget_sensitivity'][0][0]:,.0f}", f"{R['budget_sensitivity'][-1][3]:,.2f}"),
         "early_warning": "The first five assessed budgets averaging below $20,000",
         "register_id": "RSK-003"},
        {"concern": "Support worker wages sit at SCHADS Level 3 rather than Level 2",
         "likelihood": "Possible", "impact": "Moderate",
         "mitigation": "Classify against the award before the first roster; the sensitivity shows the NDIS margin falling from $%s to $%s an hour, a twenty-nine per cent reduction" % (
             f"{ndis_contrib_hr_owner:,.2f}", "15.15"),
         "early_warning": "A candidate holding a Certificate IV or supervising others, which triggers the higher classification",
         "register_id": "RSK-004"},
        {"concern": "Two entities carry a fixed cost the venture does not need",
         "likelihood": "Almost certain", "impact": "Minor",
         "mitigation": "The separation is a deliberate choice for liability isolation, priced at $%s a year; it is revisited if either entity fails to reach its break-even client or hour count within twelve months" % f"{S['annual_cost_of_separation']:,.2f}",
         "early_warning": "Either entity still below break-even at month twelve",
         "register_id": "RSK-005"},
        {"concern": "Aged care sector-wide losses indicate a structural problem the model has missed",
         "likelihood": "Possible", "impact": "Severe",
         "mitigation": "Independent verification established that the losses sit in legacy overhead and the replaced fee structure, not in the direct-services line, which remains positive at about $16.10 per client per day — $63.85 of revenue against $47.75 of direct cost (SRC-074); a new entrant carries neither. This remains the single largest exposure in the plan and is re-tested against each quarterly sector financial report",
         "early_warning": "The direct-services line turning negative in the published sector report, as distinct from the total result",
         "register_id": "RSK-006"}
      ],

      "efficiency_audit": {
        "alternatives": [
          {"option": "Aged care entity first, NDIS entity second",
           "cost": "$%s one-off, $%s a month fixed" % (f"{R['one_off'][1]:,.2f}", f"{R['monthly_fixed'][1]:,.2f}"),
           "time": "Contribution from month four",
           "risk": "Sector-wide losses are real and the entrant advantage is assumed, not proven",
           "verdict": "RECOMMENDED — break-even at one client, %s times the NDIS contribution per client, and the lighter payment lag" % R["contribution_ratio_paid_admin"]},
          {"option": "NDIS entity first, aged care second",
           "cost": "$%s one-off, $%s a month fixed" % (f"{NR['one_off_core'][1]:,.2f}", f"{NR['monthly_fixed'][1]:,.2f}"),
           "time": "Cash positive month %s on the base ramp; never within twelve months with paid administration" % NR["base_month_positive"],
           "risk": "Contribution collapses to $%s an hour once administration is paid and turns negative at 0.40 hours" % f"{ndis_contrib_hr_paid:,.2f}",
           "verdict": "NOT RECOMMENDED FIRST — the same capital buys a weaker and more fragile return"},
          {"option": "Support coordination only, no support workforce",
           "cost": "$%s one-off, $%s a month fixed" % (f"{NR['one_off_sc'][1]:,.2f}", f"{NR['monthly_fixed'][1]:,.2f}"),
           "time": "Break-even at %s hours a month" % NR["sc"]["breakeven_hours_per_month"],
           "risk": "Income is capped by the owner's own available hours and does not scale",
           "verdict": "RECOMMENDED AS A PARALLEL LINE — lowest capital, no wage exposure, but it is a job rather than a business"},
          {"option": "Do not start; remain employed",
           "cost": "$0 committed",
           "time": "Immediate",
           "risk": "Forgoes a modelled three-year net of $%s after the owner's foregone income is charged" % f"{net:,.2f}",
           "verdict": "THE BASELINE — every other option must beat this, and on the modelled figures the combined venture does"}
        ],
        "build_vs_buy": (
          "Buy the compliance layer, build the operating layer. The policy and procedure manuals "
          "are bought at $4,997 an entity rather than written, because a rejected audit costs more "
          "than the manual and the regulator's expectations are not discoverable from outside. "
          "Rostering, claiming and care-management software is bought at $%s a month combined. "
          "Administration is neither bought nor built in year one: the owner does it, because that "
          "is the single decision that separates a positive NDIS contribution from a negative one."
          % f"{N.RECURRING_MONTHLY[0][2] + AC.RECURRING_MONTHLY[0][2]:,.2f}"),
        "recommendation": (
          "Start the aged care entity first and run support coordination alongside it from the "
          "owner's own hours. Register the NDIS entity in parallel but do not take core-support "
          "clients until the aged care line covers the combined fixed cost of $%s a month. Hold "
          "administration with the owner until the NDIS line clears %s billable hours a month."
          % (f"{S['two_entities_fixed_month']:,.2f}", NR["breakeven_hrs_paid_admin"])),
        "date": datetime.date.today().isoformat()
      },

      "value_validation": {
        "verdict": "DECISION OWED BY OWNER",
        "justification": (
          "The modelled three-year net is $%s on a total cost of $%s, a return of %s per cent, "
          "with cash break-even at month %s. That clears the do-nothing baseline. It is not "
          "converted to PROCEED here because the two figures the case rests on — the average "
          "assessed budget of $%s a year and the administration load of %s hours per billable "
          "hour — are both Low confidence assumptions, and because a verdict declared by the "
          "party that built the model is not a verdict (GOV-C3.2). The owner decides."
          % (f"{money(net):,.2f}", f"{money(total_cost):,.2f}", round(roi, 1),
             be_cash if be_cash else "not reached",
             f"{AC.v(AC.DRIVERS,'avg_client_budget'):,.0f}",
             AC.v(AC.DRIVERS, "admin_hours_per_billable_hour"))),
        "conditions": [
          "Confirm the average assessed classification budget against the first five real referrals before committing beyond the aged care registration cost",
          "Hold $%s in combined runway and $%s in working capital, uncommitted, before the first worker is engaged" % (
              f"{NR['runway_6mo_core_no_draw'] + R['runway_6mo_no_draw']:,.2f}",
              f"{NR['working_capital_at_300h'] + R['working_capital']:,.2f}"),
          "Keep administration with the owner until the NDIS entity clears %s billable hours a month" % NR["breakeven_hrs_paid_admin"],
          "Re-run both models when the next NDIS price guide or IHACPA advisory rate is published; neither is stable across a financial year",
          "Accept the $%s three-year cost of holding two entities as the price of liability separation, or collapse to one entity" % f"{S['three_year_cost_of_separation']:,.2f}"
        ],
        "what_would_change_this": [
          "An average assessed budget of $20,000 a year, which cuts the contribution per client by 36 per cent to $638.83; it does not halve until about $16,200 (Table 15.2)",
          "Administration running at 0.40 hours per billable hour, which makes the NDIS line lose $2.07 an hour",
          "A published sector report showing the aged care direct-services line negative, not only the total result",
          "Award classification landing at SCHADS Level 3, which removes twenty-nine per cent of the NDIS margin"
        ]
      },

      "footer": {
        "generated_from": "01_System/build_business_plan_json.py, projecting model_params.py and model_agedcare.py",
        "generator_version": "1.0.0",
        "produced_by": "Builder agent, %s" % datetime.date.today().isoformat(),
        "verified_by": _verified_by()
      }
    }

    # DEF-033. There used to be a repair line here that overwrote a placeholder written 250 lines
    # above — the delivered JSON was correct only because of it, and any refactor that dropped the
    # patch would have shipped "NDIS $0" onto the A3 page. The value is now written once, where it
    # belongs. A fact repaired after the fact is a fact waiting to break.
    return d


if __name__ == "__main__":
    data = build()
    with open(OUT, "w", encoding="utf-8") as f:
        json.dump(data, f, indent=2, ensure_ascii=False)
    print("wrote %s (%d bytes)" % (os.path.relpath(OUT, os.path.dirname(HERE)),
                                   os.path.getsize(OUT)))