Files
TheTree_mock/tests/economy/test_p05_funds_runtime.gd
T

754 lines
49 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
extends SceneTree
const FixtureCleanup := preload("res://tests/helpers/ui_audio_fixture_cleanup.gd")
## P05-02: front (表金) and hidden (裏金) funds, the A09 payment rule, the A10 rank
## tax, the A11 loan interest, the three business day reconciliation and saves.
## The policy and reconciliation scenes use explicit ledger fixtures for sales,
## debt and drains, with the production Operations closes. The repair 2 scenes
## run the production paths themselves: built and staffed facilities whose guests
## complete their services, the automatic purchase, the repayment command and the
## legacy 00:00 upkeep; a fixture there only sets a balance (a loan, a drain), never
## the payment under test. The last check opens the ledger from the game's own Main
## (F7), so the runner gives this suite the ordinary Main audio cleanup proof
## (tools/regression/suite-policy.json).
const FRONT := &"PLAYER_CASH"
const BACK := &"BACK_CASH"
const LOAN := &"LOAN_LIABILITY:p05.fixture"
const FRONT_SALES := &"P05_FIXTURE_FRONT_SALES"
const BACK_SALES := &"P05_FIXTURE_BACK_SALES"
const PAYER := &"P05_FIXTURE_PAYER"
const SINK := &"P05_FIXTURE_SINK"
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("P05-02: front and hidden funds")
_test_policy_values()
_test_sale_fund_routing()
_test_three_business_days()
_test_resume_matches_uninterrupted()
_test_old_save_migration()
_test_repayment_top_up()
_test_purchase_top_up()
_test_upkeep_top_up()
_test_actual_service_funds()
_test_operation_top_ups_and_day_total()
_test_legacy_midnight_top_up()
_test_tax_arrears_next_day()
await _test_ledger_opened_from_main()
await process_frame
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _expect(condition: bool, message: String) -> void:
_checks += 1
if not condition:
_failures += 1
push_error(message)
func _new_fixture(seed: int) -> Dictionary:
var clock: Variant = load("res://autoload/game_clock.gd").new()
var state: Variant = load("res://autoload/game_state.gd").new()
var runtime: Variant = load("res://autoload/simulation_runtime.gd").new()
clock.reset(1, 6, 0)
state.reset()
_expect(bool(runtime.initialize(clock, state, seed).get(&"valid", false)), "fixture Runtime initializes")
clock.set_paused(true)
runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
_expect(bool(runtime.enable_production_operations().get(&"ok", false)), "fixture enables production Operations")
for account: StringName in [FRONT_SALES, BACK_SALES, PAYER, SINK]:
runtime.economy_service.register_account(account)
runtime.economy_service.register_account(LOAN, true)
# Guests' Gold enters the ledger only through explicit initialization.
_expect(runtime.economy_service.initialize_account(PAYER, 100000, clock.absolute_minute, &"p05.fixture.payer").is_success(), "fixture payer funded")
return {&"clock": clock, &"state": state, &"runtime": runtime}
func _free_fixture(fixture: Dictionary) -> void:
for key: StringName in [&"runtime", &"clock", &"state"]:
if fixture.get(key) != null:
fixture[key].free()
# Same accelerated boundary traversal as tests/mvp/test_runtime_operations_acceptance.gd:
# the fixtures hold no Facilities, Staff or logistics, only ledger postings, which
# are made on 30-minute marks.
func _advance(fixture: Dictionary, amount: int) -> void:
var clock: Variant = fixture[&"clock"]
var runtime: Variant = fixture[&"runtime"]
var advanced := 0
while advanced < amount:
# Every Operations boundary is on a 30-minute mark; each step ends on one.
var step := mini(30 - int(clock.absolute_minute) % 30, amount - advanced)
var target: int = clock.absolute_minute + step
clock.absolute_minute = target
clock.call("_sync_calendar_fields")
clock.snapshot_version += step
runtime.call("_advance_weather", target)
runtime.call("_advance_facility_upkeep", target)
runtime.call("_advance_operations_time", target)
_align_clocks(runtime, target)
advanced += step
# Same as tests/mvp/test_runtime_operations_acceptance.gd: the fixture owns no
# actors, cars or rides, so only the empty aggregates' clocks follow the minute.
func _align_clocks(runtime: Variant, minute: int) -> void:
for service: Variant in [runtime.inventory_service, runtime.facility_service, runtime.kitchen_production,
runtime.guest_service, runtime.staff_service, runtime.authority_service, runtime.defense_service,
runtime.construction_service, runtime.excavation_service]:
service.current_minute = minute
var transport_snapshot: Dictionary = runtime.elevators.create_snapshot()
if not (transport_snapshot[&"shafts"] as Array).is_empty() or not (transport_snapshot[&"trips"] as Array).is_empty():
_expect(false, "accelerated fixture cannot skip live elevator movement")
return
transport_snapshot[&"current_minute"] = minute
if not runtime.elevators.restore_snapshot(transport_snapshot):
_expect(false, "empty transport clock aligns through validated snapshot restore")
func _advance_to(fixture: Dictionary, minute: int) -> void:
_advance(fixture, minute - int(fixture[&"clock"].absolute_minute))
func _sale(fixture: Dictionary, revenue_account: StringName, fund: StringName, amount: int, key: String) -> bool:
var runtime: Variant = fixture[&"runtime"]
var minute: int = fixture[&"clock"].absolute_minute
var entries: Array = [
LedgerEntry.new().configure(&"", minute, revenue_account, PAYER, amount, &"SERVICE_SALE", &"p05.fixture"),
LedgerEntry.new().configure(&"", minute, fund, revenue_account, amount, &"SERVICE_CASH_SWEEP", &"p05.fixture"),
]
return runtime.economy_service.post(entries, StringName("p05.sale." + key)).is_success()
func _move(fixture: Dictionary, from: StringName, to: StringName, amount: int, category: StringName, key: String) -> bool:
var runtime: Variant = fixture[&"runtime"]
return runtime.economy_service.transfer(from, to, amount, fixture[&"clock"].absolute_minute, category,
&"p05.fixture", StringName("p05.move." + key)).is_success()
func _fund_delta(runtime: Variant, account: StringName, after_minute: int, through_minute: int) -> int:
var delta := 0
for entry: LedgerEntry in runtime.economy_service.get_entries():
if entry.absolute_minute <= after_minute or entry.absolute_minute > through_minute:
continue
if entry.debit_account == account:
delta += entry.amount
if entry.credit_account == account:
delta -= entry.amount
return delta
func _test_policy_values() -> void:
_expect(FinancePolicy.rank_tax(800) == 80 and FinancePolicy.rank_tax(109) == 10 and FinancePolicy.rank_tax(9) == 0, "A10 rank tax is 10% rounded down")
_expect(FinancePolicy.rank_tax(-5) == 0, "no tax on a negative day")
_expect(FinancePolicy.loan_interest(1000) == 10 and FinancePolicy.loan_interest(199) == 1 and FinancePolicy.loan_interest(99) == 0, "A11 interest is 1% per day rounded down")
func _test_sale_fund_routing() -> void:
var fixture := _new_fixture(5021)
var runtime: Variant = fixture[&"runtime"]
var back_ids: Array[StringName] = []
var front_ids: Array[StringName] = []
# Expansion definitions are read from their files; they are not registered by default.
for path: String in ["res://data/facilities/adult_private_room.tres", "res://data/facilities/expansion/black_market.tres",
"res://data/facilities/expansion/vip_private_salon.tres", "res://data/facilities/cafeteria.tres"]:
var definition: Variant = load(path)
if definition == null:
continue
if FinancePolicy.is_back_fund_service(definition.tags):
back_ids.append(definition.id)
else:
front_ids.append(definition.id)
_expect(back_ids.has(&"adult_private_room") and back_ids.has(&"black_market") and back_ids.has(&"vip_private_salon"), "adult, trade and private salon services earn hidden funds")
_expect(front_ids.has(&"cafeteria"), "dining earns front funds")
_expect(runtime.economy_service.has_account(BACK) and not runtime.economy_service.account_allows_negative(BACK), "the hidden fund is its own non-negative account")
_expect(runtime.economy_service.get_balance(BACK) == 0 and runtime.economy_service.get_balance(FRONT) == 5000, "a new game starts with every Gold in front funds")
_free_fixture(fixture)
func _test_three_business_days() -> void:
var fixture := _new_fixture(5022)
var runtime: Variant = fixture[&"runtime"]
var state: Variant = fixture[&"state"]
var coordinator: Variant = runtime.operations_coordinator
# Day 1: borrow 1000, front sale 300, hidden sale 500.
_advance_to(fixture, 420)
_expect(_move(fixture, LOAN, FRONT, 1000, &"LOAN_PRINCIPAL_BORROWED", "borrow"), "fixture principal borrowed")
_expect(_sale(fixture, FRONT_SALES, FRONT, 300, "d1.front") and _sale(fixture, BACK_SALES, BACK, 500, "d1.back"), "day 1 sales")
_advance_to(fixture, 1440)
var day1: Dictionary = coordinator.get_day_summary(1)
_expect(not day1.is_empty(), "day 1 closes at 00:00")
var totals1: Dictionary = day1.get(&"category_totals", {})
_expect(int(totals1.get(&"revenue", -1)) == 800, "day 1 sales are front + hidden")
_expect(int(totals1.get(&"taxes_and_interest", -1)) == 90, "day 1 rank tax 80 and interest 10")
_expect(int(totals1.get(&"financing_principal_inflows", -1)) == 1000, "principal stays financing, outside sales")
var funds1: Dictionary = day1.get(&"settlement", {}).get(&"funds", {})
_expect(int(funds1.get(&"revenue_front", -1)) == 300 and int(funds1.get(&"revenue_back", -1)) == 500, "day 1 fund report splits sales")
_expect(int(funds1.get(&"rank_tax", -1)) == 80 and int(funds1.get(&"rank_tax_paid", -1)) == 80, "day 1 rank tax paid in full")
_expect(int(funds1.get(&"loan_interest", -1)) == 10 and int(funds1.get(&"loan_interest_paid", -1)) == 10, "day 1 interest paid in full")
_expect(int(funds1.get(&"back_top_up", -1)) == 0, "front funds alone paid day 1")
_expect(runtime.economy_service.get_balance(FRONT) == 6210 and runtime.economy_service.get_balance(BACK) == 500, "day 1 balances")
_expect(state.get_resource(&"gold") == runtime.economy_service.get_balance(FRONT), "Gold mirrors front funds")
# The starting Gold is posted at 06:00 (minute 360); day 1 changes follow it.
_expect(_fund_delta(runtime, FRONT, 360, 1440) == 1210 and _fund_delta(runtime, BACK, 360, 1440) == 500, "day 1 fund changes equal the ledger")
# Day 2: front funds fall to 5, so 5 of the 10 interest comes from hidden funds.
_advance_to(fixture, 1440 + 600)
_expect(_move(fixture, FRONT, &"OPERATING_EXPENSE", 6205, &"OPERATING_EXPENSE", "d2.cost"), "day 2 purchase-like cost")
_advance_to(fixture, 2880)
var funds2: Dictionary = coordinator.get_day_summary(2).get(&"settlement", {}).get(&"funds", {})
_expect(int(funds2.get(&"back_top_up", -1)) == 5, "A09: hidden funds cover the 5 front funds cannot pay")
_expect(int(funds2.get(&"loan_interest_paid", -1)) == 10 and int(funds2.get(&"rank_tax", -1)) == 0, "day 2 interest paid, no sales no tax")
_expect(runtime.economy_service.get_balance(FRONT) == 0 and runtime.economy_service.get_balance(BACK) == 495, "day 2 balances")
_expect(int(coordinator.get_day_summary(2)[&"category_totals"][&"revenue"]) == 0, "the top-up is not sales")
_expect(_fund_delta(runtime, FRONT, 1440, 2880) == -6210 and _fund_delta(runtime, BACK, 1440, 2880) == -5, "day 2 fund changes equal the ledger")
# Day 3: hidden funds are gone too, so the interest stays unpaid.
_advance_to(fixture, 2880 + 600)
_expect(_move(fixture, BACK, SINK, 495, &"TRANSFER", "d3.drain"), "day 3 hidden funds drained")
_advance_to(fixture, 4320)
var day3: Dictionary = coordinator.get_day_summary(3)
var funds3: Dictionary = day3.get(&"settlement", {}).get(&"funds", {})
_expect(int(funds3.get(&"loan_interest", -1)) == 10 and int(funds3.get(&"loan_interest_paid", -1)) == 0 and int(funds3.get(&"loan_interest_arrears", -1)) == 10, "A09: unpaid interest carries over")
_expect(int(day3[&"settlement"][&"arrears_total"]) == 10, "day 3 arrears include the interest")
_expect(int(runtime.get_operations_report().get(&"arrears_total", -1)) == 10, "Operations report sees the interest arrears")
_expect(int(day3[&"category_totals"][&"taxes_and_interest"]) == 10, "the unpaid interest is still the day's expense")
# Day 4: a front sale pays the older interest first.
_advance_to(fixture, 4320 + 600)
_expect(_sale(fixture, FRONT_SALES, FRONT, 100, "d4.front"), "day 4 sale")
_advance_to(fixture, 5760)
var funds4: Dictionary = coordinator.get_day_summary(4).get(&"settlement", {}).get(&"funds", {})
_expect(int(funds4.get(&"rank_tax", -1)) == 10 and int(funds4.get(&"rank_tax_paid", -1)) == 10, "day 4 tax paid")
_expect(int(funds4.get(&"loan_interest_paid", -1)) == 20 and int(funds4.get(&"loan_interest_arrears", -1)) == 0, "day 4 pays the older interest and the new one")
_expect(runtime.economy_service.get_balance(FRONT) == 70, "day 4 front balance")
_expect(runtime.economy_service.validate_ledger()[&"ok"], "the ledger reconciles after four closes")
_check_ledger_panel(runtime, fixture[&"clock"])
var saved: Dictionary = runtime.create_snapshot()
_expect(runtime.restore_snapshot(saved) and var_to_bytes(saved) == var_to_bytes(runtime.create_snapshot()), "four closed days restore exactly")
_free_fixture(fixture)
func _scripted_days(fixture: Dictionary, restore_at: Array[int], through_files := false) -> Dictionary:
var runtime: Variant = fixture[&"runtime"]
# Postings and save/restore points in time order; the last minute closes day 3.
var steps: Array = [
[420, func() -> void: _move(fixture, LOAN, FRONT, 1000, &"LOAN_PRINCIPAL_BORROWED", "borrow")],
[450, func() -> void: _sale(fixture, BACK_SALES, BACK, 700, "d1.back")],
[1440, Callable()],
[2040, func() -> void: _move(fixture, FRONT, &"OPERATING_EXPENSE", 5900, &"OPERATING_EXPENSE", "d2.cost")],
[2400, Callable()],
[2880, Callable()],
[3510, func() -> void: _sale(fixture, FRONT_SALES, FRONT, 50, "d3.front")],
[4320, Callable()],
]
for step: Array in steps:
var minute := int(step[0])
_advance_to(fixture, minute)
if (step[1] as Callable).is_valid():
(step[1] as Callable).call()
if minute in restore_at and through_files:
# The ordinary save file path: write the save, then load it back.
var path := "user://p05_resume_%d.save" % minute
_expect(bool(runtime.save_to_path(path).get(&"ok", false)), "save file at minute %d" % minute)
_expect(bool(runtime.load_from_path(path).get(&"ok", false)), "load the save file at minute %d" % minute)
elif minute in restore_at:
var saved: Dictionary = runtime.create_snapshot()
_expect(runtime.restore_snapshot(saved), "restore at minute %d" % minute)
return runtime.create_snapshot()
func _test_resume_matches_uninterrupted() -> void:
var straight := _new_fixture(5023)
var resumed := _new_fixture(5023)
var a := _scripted_days(straight, [])
var b := _scripted_days(resumed, [1440, 2400, 2880])
_expect(var_to_bytes(a) == var_to_bytes(b), "saving mid-day and at 00:00 changes nothing: no double tax, interest or top-up")
var runtime: Variant = resumed[&"runtime"]
var keys := 0
for entry: LedgerEntry in runtime.economy_service.get_entries():
if entry.category in [FinancePolicy.RANK_TAX_CATEGORY, FinancePolicy.LOAN_INTEREST_CATEGORY]:
keys += 1
# Day 1: tax 70 and interest 10; day 2: interest only (no sales); day 3: tax 5 and interest 10.
_expect(keys == 5, "each tax and interest accrual is posted once (no duplicates)")
# Through save files the save and load events are new, so compare the funds side only.
var through_files := _new_fixture(5023)
var c := _scripted_days(through_files, [1440, 2400, 2880], true)
_expect(var_to_bytes(c[&"systems"][&"runtime"][&"economy"]) == var_to_bytes(a[&"systems"][&"runtime"][&"economy"]), "saving to files and loading them back leaves the ledger exactly as without saving")
var straight_days: Array = straight[&"runtime"].operations_coordinator.get_day_summaries()
var file_days: Array = through_files[&"runtime"].operations_coordinator.get_day_summaries()
# Operations history is persisted in a transport-safe form (StringName ids become
# String values), so compare as the Runtime's own restore crosslink does.
var runtime_a: Variant = straight[&"runtime"]
_expect(runtime_a.call("_strict_variants_equal", runtime_a.call("_operations_snapshot_comparison_value", straight_days),
runtime_a.call("_operations_snapshot_comparison_value", file_days)), "the daily closes, fund reports and arrears are the same through save files")
_free_fixture(straight)
_free_fixture(resumed)
_free_fixture(through_files)
func _test_old_save_migration() -> void:
var fixture := _new_fixture(5024)
var runtime: Variant = fixture[&"runtime"]
_advance_to(fixture, 420)
var current: Dictionary = runtime.create_snapshot()
var old := current.duplicate(true)
var economy: Dictionary = old[&"systems"][&"runtime"][&"economy"]
var kept: Array = []
for account: Dictionary in economy[&"accounts"]:
if String(account[&"account_id"]) not in ["BACK_CASH", "RANK_TAX_EXPENSE", "RANK_TAX_PAYABLE", "LOAN_INTEREST_EXPENSE", "LOAN_INTEREST_PAYABLE"]:
kept.append(account)
economy[&"accounts"] = kept
_expect(runtime.restore_snapshot(old), "A12: a save from before the split loads")
_expect(runtime.economy_service.get_balance(FRONT) == 5000 and runtime.economy_service.get_balance(BACK) == 0, "A12: every Gold is front funds, hidden funds start at 0")
_expect(var_to_bytes(runtime.create_snapshot()) == var_to_bytes(current), "A12: the migrated save equals a current save of the same game")
var partial := old.duplicate(true)
var partial_accounts: Array = partial[&"systems"][&"runtime"][&"economy"][&"accounts"]
partial_accounts.append({&"account_id": "RANK_TAX_PAYABLE", &"allow_negative": true, &"system": false})
var before: Dictionary = runtime.create_snapshot()
_expect(not runtime.restore_snapshot(partial) and var_to_bytes(before) == var_to_bytes(runtime.create_snapshot()), "a partial set of fund accounts is rejected and changes nothing")
_free_fixture(fixture)
func _test_repayment_top_up() -> void:
var fixture := _new_fixture(5025)
var runtime: Variant = fixture[&"runtime"]
_advance_to(fixture, 420)
_expect(_move(fixture, LOAN, FRONT, 1000, &"LOAN_PRINCIPAL_BORROWED", "borrow"), "fixture principal")
_expect(_sale(fixture, BACK_SALES, BACK, 300, "back") and _move(fixture, FRONT, &"OPERATING_EXPENSE", 5980, &"OPERATING_EXPENSE", "cost"), "front 20, hidden 300")
var receipts: Array = []
runtime.economy_service.transaction_posted.connect(func(id: StringName, _entries: Array) -> void: receipts.append(id))
var before: Dictionary = runtime.create_snapshot()
_expect(not runtime.repay_loan_principal(&"p05.fixture", 400, &"p05.repay.too_much").is_success() and var_to_bytes(before) == var_to_bytes(runtime.create_snapshot()), "front + hidden short: repayment rejected, nothing changes")
var paid: CommandResult = runtime.repay_loan_principal(&"p05.fixture", 200, &"p05.repay")
_expect(paid.is_success() and int(paid.details.get(&"outstanding_principal", -1)) == 800, "repayment uses hidden funds for the rest")
_expect(runtime.economy_service.get_balance(FRONT) == 0 and runtime.economy_service.get_balance(BACK) == 120, "front 20 first, then 180 from hidden funds")
_expect(receipts.size() == 1, "payment and top-up are one transaction")
var entries: Array = runtime.economy_service.get_transaction_entries(StringName(paid.details[&"transaction_id"]))
_expect(entries.size() == 2 and entries[0].category == FinancePolicy.TOP_UP_CATEGORY and entries[1].category == &"LOAN_PRINCIPAL_REPAID", "top-up then principal")
_expect(fixture[&"state"].get_resource(&"gold") == 0, "Gold mirrors front funds after repayment")
_free_fixture(fixture)
func _test_purchase_top_up() -> void:
var clock: Variant = load("res://autoload/game_clock.gd").new()
var state: Variant = load("res://autoload/game_state.gd").new()
var runtime: Variant = load("res://autoload/simulation_runtime.gd").new()
clock.reset(1, 6, 0)
state.reset()
_expect(bool(runtime.initialize(clock, state, 5026).get(&"valid", false)), "purchase fixture Runtime initializes")
clock.set_paused(true)
runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
for row: Array in [[&"warehouse", Vector3i(-8, 0, 1)], [&"staff_door", Vector3i(0, 0, 1)]]:
_expect(runtime.submit_command(&"build_facility", {&"definition_id": row[0], &"origin": row[1]}).is_success(), "build %s" % row[0])
clock.advance_minutes(180)
var warehouse := &""
for facility: FacilityInstance in runtime.facility_service.get_instances():
if facility.definition_id == &"warehouse":
warehouse = facility.instance_id
_expect(not warehouse.is_empty(), "warehouse completes")
var fixture := {&"clock": clock, &"state": state, &"runtime": runtime}
for account: StringName in [BACK_SALES, PAYER]:
runtime.economy_service.register_account(account)
_expect(runtime.economy_service.initialize_account(PAYER, 1000, clock.absolute_minute, &"p05.purchase.payer").is_success(), "fixture payer")
var front: int = runtime.economy_service.get_balance(FRONT)
_expect(_move(fixture, FRONT, &"OPERATING_EXPENSE", front - 2, &"OPERATING_EXPENSE", "purchase.drain") and _sale(fixture, BACK_SALES, BACK, 100, "purchase.back"), "front 2, hidden 100")
var preview: Dictionary = runtime.preview_supply_purchase(warehouse, SupplyCatalog.RAW_FOOD, 5)
_expect(bool(preview.get(&"ok", false)) and int(preview[&"total_cost"]) == 15 and int(preview[&"cash_after"]) == 87, "A09: the quote counts front + hidden funds")
var paid: CommandResult = runtime.submit_command(&"purchase_supplies", {&"warehouse_id": warehouse, &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 5})
_expect(paid.is_success(), "the purchase uses hidden funds for what front funds cannot pay")
_expect(runtime.economy_service.get_balance(FRONT) == 0 and runtime.economy_service.get_balance(BACK) == 87, "front 2 first, then 13 from hidden funds")
var purchase_entries: Array = runtime.economy_service.get_transaction_entries(StringName(paid.details.get(&"transaction_id", &"")))
_expect(purchase_entries.size() == 1 and purchase_entries[0].credit_account == FRONT and purchase_entries[0].amount == 15, "the purchase receipt keeps its single front-fund entry")
var top_ups := 0
for entry: LedgerEntry in runtime.economy_service.get_entries():
if entry.category == FinancePolicy.TOP_UP_CATEGORY:
top_ups += entry.amount
_expect(top_ups == 13, "one 13 Gold top-up from hidden funds")
_expect(state.get_resource(&"gold") == 0, "Gold mirrors front funds after the purchase")
# A refused command is still recorded as processed; the funds and stock must not move.
var economy_before: Dictionary = runtime.economy_service.create_snapshot()
var inventory_before: Dictionary = runtime.inventory_service.create_snapshot()
var refused: CommandResult = runtime.submit_command(&"purchase_supplies", {&"warehouse_id": warehouse, &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 40})
_expect(not refused.is_success() and StringName(refused.details.get(&"reason", &"")) == &"INSUFFICIENT_FUNDS", "front + hidden short: purchase refused for funds")
_expect(var_to_bytes(economy_before) == var_to_bytes(runtime.economy_service.create_snapshot()) \
and var_to_bytes(inventory_before) == var_to_bytes(runtime.inventory_service.create_snapshot()), "the refused purchase moves no Gold and no stock")
var saved: Dictionary = runtime.create_snapshot()
_expect(runtime.restore_snapshot(saved) and var_to_bytes(saved) == var_to_bytes(runtime.create_snapshot()), "purchase with a top-up restores exactly")
runtime.free()
clock.free()
state.free()
func _test_upkeep_top_up() -> void:
var fixture := _new_fixture(5027)
var runtime: Variant = fixture[&"runtime"]
var clock: Variant = fixture[&"clock"]
_expect(runtime.submit_command(&"build_facility", {&"definition_id": &"warehouse", &"origin": Vector3i(-8, 0, 1)}).is_success(), "build a warehouse")
clock.advance_minutes(180)
_advance_to(fixture, 600)
var front: int = runtime.economy_service.get_balance(FRONT)
_expect(_move(fixture, FRONT, &"OPERATING_EXPENSE", front, &"OPERATING_EXPENSE", "upkeep.drain") and _sale(fixture, BACK_SALES, BACK, 1000, "upkeep.back"), "front 0, hidden 1000")
_advance_to(fixture, 1440)
var summary: Dictionary = runtime.operations_coordinator.get_day_summary(1)
var maintenance := int(summary.get(&"category_totals", {}).get(&"maintenance", -1))
var funds: Dictionary = summary.get(&"settlement", {}).get(&"funds", {})
_expect(maintenance > 0, "the warehouse owes upkeep for day 1")
_expect(int(funds.get(&"back_top_up", -1)) == maintenance + int(funds.get(&"rank_tax", 0)), "A09: hidden funds cover the upkeep (and tax) front funds cannot pay")
_expect(runtime.upkeep_settlement.get_arrears_total() == 0 and int(summary[&"settlement"][&"arrears_total"]) == 0, "no upkeep arrears")
_expect(runtime.economy_service.get_balance(FRONT) == 0 and runtime.economy_service.get_balance(BACK) == 1000 - int(funds[&"back_top_up"]), "front 0, hidden paid the rest")
_free_fixture(fixture)
func _command(runtime: Variant, type: StringName, payload: Dictionary) -> bool:
var result: CommandResult = runtime.submit_command(type, payload)
if not result.is_success():
push_error("%s: %s %s" % [type, result.message, result.details.get(&"reason", &"")])
return result.is_success()
func _facility_of(runtime: Variant, definition_id: StringName) -> StringName:
for facility: FacilityInstance in runtime.facility_service.get_instances():
if facility.definition_id == definition_id:
return facility.instance_id
return &""
func _day_funds(runtime: Variant, day: int) -> Dictionary:
return runtime.operations_coordinator.get_day_summary(day).get(&"settlement", {}).get(&"funds", {})
func _closed_ledger_text(runtime: Variant, clock: Variant) -> String:
var panel := OperatingLedgerPanel.new()
root.add_child(panel)
panel.setup(runtime, clock)
panel.select_latest_close()
var text: String = panel._closed_label.text
panel.queue_free()
return text
# F1/F5 and F2 (wages) on the production paths. A front facility (massage_room,
# staffed by an ordinary hire on a DAY assignment) and the registered hidden-service
# facility (adult_private_room, underground, its service_attendant on a NIGHT
# assignment) are built and open; the arriving guests choose and complete the
# services themselves and the 00:00 closes settle the day.
func _test_actual_service_funds() -> void:
var fixture := _new_fixture(5031)
var runtime: Variant = fixture[&"runtime"]
var clock: Variant = fixture[&"clock"]
var economy: Variant = runtime.economy_service
# Rank 2 opens the underground, by the original room-zone suite's own route.
_expect(_command(runtime, &"change_resources", {&"deltas": {&"gold": 5000, &"materials": 100}})
and _command(runtime, &"update_progression_metrics", {&"metrics": {&"reputation": 25}})
and _command(runtime, &"set_world_tree_rank", {&"rank": 2}), "rank 2 through the public commands")
for x: int in range(0, 4):
_expect(_command(runtime, &"start_excavation", {&"cell": Vector3i(x, -1, 0)}), "excavate (%d, -1)" % x)
clock.advance_minutes(60)
# Guests go down by a guest corridor and stairs, staff by a service corridor and stairs.
_expect(_command(runtime, &"build_facility", {&"definition_id": &"guest_corridor", &"origin": Vector3i(0, -1, 0)}), "underground guest corridor")
for x: int in range(1, 4):
_expect(_command(runtime, &"build_facility", {&"definition_id": &"service_corridor", &"origin": Vector3i(x, -1, 1)}), "underground service corridor (%d, -1)" % x)
clock.advance_minutes(30)
_expect(_command(runtime, &"build_facility", {&"definition_id": &"guest_stairs", &"origin": Vector3i(0, 0, 0)})
and _command(runtime, &"build_facility", {&"definition_id": &"service_stairs", &"origin": Vector3i(3, 0, 1)}), "stairs down for guests and staff")
clock.advance_minutes(30)
_expect(not runtime.path_service.find_path(Vector3i(-2, 0, 0), Vector3i(0, -1, 0)).is_empty(), "guests can walk down to the room")
_expect(_command(runtime, &"build_facility", {&"definition_id": &"adult_private_room", &"origin": Vector3i(1, -1, 0)})
and _command(runtime, &"build_facility", {&"definition_id": &"massage_room", &"origin": Vector3i(-6, 0, 0)}), "build the hidden-service and the front-service facility")
clock.advance_minutes(240)
var back_room := _facility_of(runtime, &"adult_private_room")
var front_room := _facility_of(runtime, &"massage_room")
_expect(not back_room.is_empty() and not front_room.is_empty(), "both facilities complete")
_expect(_command(runtime, &"hire_staff_candidate", {&"candidate_id": &"staff_petra", &"current_cell": Vector3i(-5, 0, 0)})
and _command(runtime, &"set_staff_work_assignment", {&"staff_id": &"staff_petra", &"facility_id": front_room, &"template_id": &"DAY"}), "an ordinary hire works the massage room by day")
# No hire candidate holds the room's service_attendant role (REPAIR_02.md), so it
# is registered through the Runtime's register_staff command.
_expect(_command(runtime, &"register_staff", {&"staff_id": &"p05_attendant", &"role_id": &"service_attendant", &"options": {
&"current_cell": Vector3i(3, 0, 1), &"state": StaffRuntime.State.OFF_DUTY,
&"permitted_task_tags": [&"service", &"work", &"adult_service"], &"equipment_tags": [&"uniform"],
&"role_tags": [&"service_attendant"], &"skills": {&"service": 75}}})
and _command(runtime, &"set_staff_work_assignment", {&"staff_id": &"p05_attendant", &"facility_id": back_room, &"template_id": &"NIGHT"}), "the room's attendant works by night")
clock.advance_minutes(721 - int(clock.absolute_minute))
_expect(runtime.facility_service.get_instance(front_room).state == FacilityInstance.State.OPEN, "the massage room opens with its day staff")
clock.advance_minutes(1141 - int(clock.absolute_minute))
_expect(runtime.facility_service.get_instance(back_room).state == FacilityInstance.State.OPEN, "the room opens with its night staff")
clock.advance_minutes(1440 - int(clock.absolute_minute))
# Day 1: every sale of the massage room went to front funds, of the room to hidden funds.
var front_sales := 0
var back_sales := 0
var misrouted := 0
var back_payers := {}
for entry: LedgerEntry in economy.get_entries():
if entry.category == &"SERVICE_SALE" and entry.source_entity_id == back_room and String(entry.credit_account).begins_with("GUEST_WALLET:guest-auto-"):
back_payers[entry.credit_account] = true
if entry.category != &"SERVICE_CASH_SWEEP" or entry.absolute_minute >= 1440:
continue
if entry.source_entity_id == front_room and entry.debit_account == FRONT:
front_sales += entry.amount
elif entry.source_entity_id == back_room and entry.debit_account == BACK:
back_sales += entry.amount
else:
misrouted += 1
_expect(front_sales > 0 and back_sales > 0 and misrouted == 0, "front service sales sweep to PLAYER_CASH, hidden ones to BACK_CASH (front %d, hidden %d)" % [front_sales, back_sales])
_expect(not back_payers.is_empty(), "arriving guests went down to the room and paid for its service themselves (%d guests)" % back_payers.size())
var day1: Dictionary = runtime.operations_coordinator.get_day_summary(1)
var funds1 := _day_funds(runtime, 1)
_expect(int(funds1.get(&"revenue_front", -1)) == front_sales and int(funds1.get(&"revenue_back", -1)) == back_sales, "the 00:00 close reports the day's sales by fund")
_expect(int(day1[&"category_totals"][&"revenue"]) == front_sales + back_sales, "sales are front + hidden")
_expect(int(funds1.get(&"rank_tax", -1)) == FinancePolicy.rank_tax(front_sales + back_sales) and int(funds1.get(&"rank_tax", -1)) == (front_sales + back_sales) / 10
and int(funds1.get(&"rank_tax_paid", -1)) == int(funds1[&"rank_tax"]), "A10: the close taxes 10% of both sales, paid")
_expect(int(funds1.get(&"back_top_up", -1)) == 0 and economy.get_balance(BACK) == back_sales and _fund_delta(runtime, BACK, 0, 1440) == back_sales, "hidden funds hold exactly the day's hidden sales")
# Day 2: the front fund is lowered to 5 by a ledger fixture before the close, so the
# close must pay the real wages, upkeep and tax with hidden funds.
clock.advance_minutes(1440 + 1430 - int(clock.absolute_minute))
_expect(_move(fixture, FRONT, &"OPERATING_EXPENSE", economy.get_balance(FRONT) - 5, &"OPERATING_EXPENSE", "service.d2.drain"), "front funds lowered to 5")
var back_before: int = economy.get_balance(BACK)
clock.advance_minutes(2880 - int(clock.absolute_minute))
var day2: Dictionary = runtime.operations_coordinator.get_day_summary(2)
var totals2: Dictionary = day2.get(&"category_totals", {})
var funds2 := _day_funds(runtime, 2)
var due := int(totals2.get(&"wages", 0)) + int(totals2.get(&"maintenance", 0)) + int(totals2.get(&"taxes_and_interest", 0))
_expect(int(totals2.get(&"wages", 0)) > 0 and int(funds2.get(&"revenue_back", 0)) > 0, "day 2 has real wages and hidden sales")
_expect(int(funds2.get(&"back_top_up", -1)) == due - 5, "A09: hidden funds cover the wages, upkeep and tax the 5 front funds cannot pay")
_expect(int(day2[&"settlement"].get(&"unpaid_after_total", -1)) == 0 and int(day2[&"settlement"][&"arrears_total"]) == 0, "the wages are paid in full, nothing is in arrears")
_expect(economy.get_balance(FRONT) == 0 and economy.get_balance(BACK) == back_before - (due - 5), "front 0, hidden funds paid the rest")
var saved: Dictionary = runtime.create_snapshot()
_expect(runtime.restore_snapshot(saved) and var_to_bytes(saved) == var_to_bytes(runtime.create_snapshot()), "the two service days restore exactly")
_free_fixture(fixture)
func _has_key(dictionary: Dictionary, key: String) -> bool:
return dictionary.has(StringName(key)) or dictionary.has(key)
func _set_key(dictionary: Dictionary, key: String, value: Variant) -> void:
if dictionary.has(key):
dictionary[key] = value
else:
dictionary[StringName(key)] = value
func _erase_key(dictionary: Dictionary, key: String) -> void:
dictionary.erase(StringName(key))
dictionary.erase(key)
# The saved fund report of a day inside a Runtime snapshot; changing it changes the snapshot.
func _saved_funds(snapshot: Dictionary, day: int) -> Dictionary:
for summary: Dictionary in snapshot[&"systems"][&"runtime"][&"operations"][&"coordinator"][&"day_summaries"]:
if int(summary.get(&"day", summary.get("day", -1))) == day:
var settlement: Dictionary = summary.get(&"settlement", summary.get("settlement", {}))
return settlement.get(&"funds", settlement.get("funds", {}))
return {}
# F2 (automatic purchase), F4 (interest after a repayment) and the day's top-up
# total: the warehouse's automatic order and the repayment command run on their
# production paths with too little front funds, then the 00:00 close. A report
# saved before the definition mark is then loaded by its own definition.
func _test_operation_top_ups_and_day_total() -> void:
var fixture := _new_fixture(5032)
var runtime: Variant = fixture[&"runtime"]
var clock: Variant = fixture[&"clock"]
var economy: Variant = runtime.economy_service
_expect(_command(runtime, &"build_facility", {&"definition_id": &"warehouse", &"origin": Vector3i(-8, 0, 1)}), "build a warehouse")
clock.advance_minutes(180)
var warehouse := _facility_of(runtime, &"warehouse")
_expect(not warehouse.is_empty(), "the warehouse completes")
_expect(_move(fixture, LOAN, FRONT, 1000, &"LOAN_PRINCIPAL_BORROWED", "ops.borrow"), "fixture principal 1000")
_expect(_move(fixture, FRONT, &"OPERATING_EXPENSE", economy.get_balance(FRONT) - 2, &"OPERATING_EXPENSE", "ops.drain")
and _sale(fixture, BACK_SALES, BACK, 1000, "ops.back"), "front 2, hidden 1000")
_expect(_command(runtime, &"set_supply_auto_purchase", {&"warehouse_id": warehouse, &"item_id": SupplyCatalog.RAW_FOOD, &"enabled": true, &"target_stock": 10}), "an automatic order for 10 raw food")
clock.advance_minutes(1)
var plan: Dictionary = runtime.auto_purchase_plans.get_plan(warehouse, SupplyCatalog.RAW_FOOD)
var receipt := SupplyAutoPurchasePlans.receipt_from_ledger(economy, warehouse, SupplyCatalog.RAW_FOOD, clock.absolute_minute, StringName(plan.get(&"last_transaction_id", &"")))
_expect(plan.get(&"last_result") == &"PURCHASED" and receipt == {&"item_id": String(SupplyCatalog.RAW_FOOD), &"quantity": 10, &"unit_price": 3, &"total_cost": 30}, "the automatic order buys 10 for 30 and its receipt verifies")
var order_key := SupplyAutoPurchasePlans.order_key(warehouse, SupplyCatalog.RAW_FOOD, clock.absolute_minute)
var order_top_up: Array = economy.get_transaction_entries(economy.get_transaction_id_for_idempotency_key(StringName("finance.back_top_up.purchase." + String(order_key))))
_expect(order_top_up.size() == 1 and order_top_up[0].category == FinancePolicy.TOP_UP_CATEGORY and order_top_up[0].amount == 28, "A09: hidden funds cover the 28 the automatic order's front funds cannot pay")
_expect(economy.get_balance(FRONT) == 0 and economy.get_balance(BACK) == 972, "front 0, hidden 972 after the automatic order")
var repaid: CommandResult = runtime.submit_command(&"repay_loan_principal", {&"loan_id": &"p05.fixture", &"amount": 200}, &"player", &"p05.fixture", 50, -1, &"p05.ops.repay")
_expect(repaid.is_success() and int(repaid.details.get(&"outstanding_principal", -1)) == 800, "the repayment command pays 200 of the 1000")
_expect(economy.get_balance(FRONT) == 0 and economy.get_balance(BACK) == 772, "hidden funds paid the repayment")
clock.advance_minutes(1440 - int(clock.absolute_minute))
var day1: Dictionary = runtime.operations_coordinator.get_day_summary(1)
var funds := _day_funds(runtime, 1)
_expect(int(funds.get(&"loan_interest", -1)) == 8 and int(funds.get(&"loan_interest_paid", -1)) == 8, "A11: the close charges 1% of the 800 left after the repayment, 8, paid")
_expect(int(funds.get(&"rank_tax", -1)) == 100 and int(funds.get(&"rank_tax_paid", -1)) == 100, "the hidden sale's tax is paid")
var close_top_up := 0
for entry: LedgerEntry in economy.get_transaction_entries(economy.get_transaction_id_for_idempotency_key(&"finance.back_top_up.day.00000001")):
close_top_up += entry.amount
var every_top_up := 0
for entry: LedgerEntry in economy.get_entries():
if entry.category == FinancePolicy.TOP_UP_CATEGORY:
every_top_up += entry.amount
_expect(close_top_up == 8 + 100 + int(day1[&"category_totals"][&"maintenance"]), "the close tops up the interest, tax and upkeep")
var total := 28 + 200 + close_top_up
_expect(int(funds.get(&"back_top_up", -1)) == total and every_top_up == total, "funds.back_top_up is the day's every top-up: the order, the repayment and the close")
_expect(int(funds.get(&"fund_report_version", -1)) == 2, "the report carries its definition version")
_expect(economy.get_balance(BACK) == 1000 - total and economy.get_balance(FRONT) == 0, "hidden funds fell by exactly the day's top-ups")
_expect(_closed_ledger_text(runtime, clock).contains("裏金から表金へ補填 %dG" % total), "the ledger's close shows the day's every top-up")
var saved: Dictionary = runtime.create_snapshot()
_expect(runtime.restore_snapshot(saved) and var_to_bytes(saved) == var_to_bytes(runtime.create_snapshot()), "the day with three top-ups restores exactly")
# A report saved before the mark (back_top_up = the close's own top-up only) loads by that definition.
var old := saved.duplicate(true)
var old_funds := _saved_funds(old, 1)
_expect(_has_key(old_funds, "fund_report_version"), "the saved report holds the mark")
_erase_key(old_funds, "fund_report_version")
_set_key(old_funds, "back_top_up", close_top_up)
_expect(runtime.restore_snapshot(old), "an unmarked report with the close-only top-up loads")
var loaded := _day_funds(runtime, 1)
_expect(int(loaded.get(&"back_top_up", -1)) == close_top_up and not _has_key(loaded, "fund_report_version"), "the loaded old report stays as it was saved")
_expect(_closed_ledger_text(runtime, clock).contains("裏金から表金へ補填(締めの時の分のみ) %dG" % close_top_up), "the ledger labels the old report's close-only top-up")
# No looser check: each definition accepts only its own value.
for case: Array in [[null, total, "an unmarked report with the day total"], [2, close_top_up, "a marked report with the close-only top-up"], [3, total, "an unknown mark"]]:
var bad := saved.duplicate(true)
var bad_funds := _saved_funds(bad, 1)
if case[0] == null:
_erase_key(bad_funds, "fund_report_version")
else:
_set_key(bad_funds, "fund_report_version", case[0])
_set_key(bad_funds, "back_top_up", case[1])
var before: Dictionary = runtime.create_snapshot()
_expect(not runtime.restore_snapshot(bad) and var_to_bytes(before) == var_to_bytes(runtime.create_snapshot()), "%s is rejected and changes nothing" % case[2])
_expect(runtime.restore_snapshot(saved) and var_to_bytes(saved) == var_to_bytes(runtime.create_snapshot()), "the current save loads again")
_free_fixture(fixture)
# F2 (legacy calendar): in a save from before the 00:00 close the upkeep is paid at
# calendar midnight and the financial day closes at 06:00 (as in
# tests/mvp/test_upkeep_legacy_midnight.gd). With no front funds the midnight upkeep
# is topped up from hidden funds, and that 06:00 day's report counts it.
func _test_legacy_midnight_top_up() -> void:
var fixture := _new_fixture(5033)
var runtime: Variant = fixture[&"runtime"]
var clock: Variant = fixture[&"clock"]
var economy: Variant = runtime.economy_service
var definition: FacilityDefinition = runtime.definition_registry.get_facility(&"guest_corridor")
var old_rate := definition.maintenance_gold
definition.maintenance_gold = 1440
_expect(_command(runtime, &"build_facility", {&"definition_id": &"guest_corridor", &"origin": Vector3i(4, 1, 0)}), "build a corridor with upkeep")
runtime.operations_coordinator._legacy_calendar_last_day = 1
clock.advance_minutes(1079)
var legacy: Dictionary = runtime.create_snapshot()
var data: Dictionary = legacy[&"systems"][&"runtime"]
data[&"runtime_schema_version"] = 9
preload("res://tests/fixtures/pre_upkeep_runtime.gd").strip_empty_upkeep(data)
data[&"operations"][&"coordinator"][&"schema_version"] = 3
data[&"operations"][&"coordinator"].erase(&"legacy_calendar_last_day")
data[&"operations"][&"coordinator"].erase(&"legacy_history_last_day")
data[&"operations"][&"coordinator"].erase(&"legacy_principal_profit_last_day")
_expect(runtime.restore_snapshot(legacy), "the legacy-calendar save loads at 23:59")
# The load replaces the Runtime's ledger service.
economy = runtime.economy_service
_expect(_move(fixture, FRONT, &"OPERATING_EXPENSE", economy.get_balance(FRONT), &"OPERATING_EXPENSE", "legacy.drain")
and _sale(fixture, BACK_SALES, BACK, 500, "legacy.back"), "front 0, hidden 500")
clock.advance_minutes(1)
var midnight: Array = economy.get_transaction_entries(economy.get_transaction_id_for_idempotency_key(&"finance.back_top_up.midnight.1440"))
_expect(midnight.size() == 1 and midnight[0].category == FinancePolicy.TOP_UP_CATEGORY and midnight[0].amount == 1, "A09: hidden funds cover the 00:00 upkeep front funds cannot pay")
_expect(economy.get_balance(FacilityUpkeepSettlement.EXPENSE) == 1 and runtime.upkeep_settlement.get_arrears_total() == 0
and economy.get_balance(FRONT) == 0 and economy.get_balance(BACK) == 499, "the midnight upkeep is paid, nothing in arrears")
clock.advance_minutes(360)
var day1: Dictionary = runtime.operations_coordinator.get_day_summary(1)
var funds := _day_funds(runtime, 1)
_expect(int(day1.get(&"closed_at", -1)) == 1800 and int(funds.get(&"rank_tax", -1)) == 50 and int(funds.get(&"rank_tax_paid", -1)) == 50, "the 06:00 close taxes the hidden sale, paid")
_expect(int(funds.get(&"back_top_up", -1)) == 51 and economy.get_balance(BACK) == 449, "the 06:00 day's report counts the midnight top-up and the close's")
var saved: Dictionary = runtime.create_snapshot()
_expect(runtime.restore_snapshot(saved) and var_to_bytes(saved) == var_to_bytes(runtime.create_snapshot()), "the legacy day restores exactly")
definition.maintenance_gold = old_rate
_free_fixture(fixture)
# F3: the day's tax that front and hidden funds cannot pay stays payable, and the
# next close pays that older tax before the new one.
func _test_tax_arrears_next_day() -> void:
var fixture := _new_fixture(5034)
var runtime: Variant = fixture[&"runtime"]
var economy: Variant = runtime.economy_service
_advance_to(fixture, 420)
_expect(_sale(fixture, FRONT_SALES, FRONT, 200, "tax.d1") and _move(fixture, FRONT, &"OPERATING_EXPENSE", economy.get_balance(FRONT), &"OPERATING_EXPENSE", "tax.d1.drain"), "day 1 sale 200, then no funds")
_advance_to(fixture, 1440)
var funds1 := _day_funds(runtime, 1)
_expect(int(funds1.get(&"rank_tax", -1)) == 20 and int(funds1.get(&"rank_tax_paid", -1)) == 0 and int(funds1.get(&"rank_tax_arrears", -1)) == 20, "A09: the unpaid tax carries over")
_expect(int(runtime.operations_coordinator.get_day_summary(1)[&"settlement"][&"arrears_total"]) == 20 and int(runtime.get_operations_report().get(&"arrears_total", -1)) == 20, "the arrears include the tax")
_advance_to(fixture, 1440 + 600)
_expect(_sale(fixture, FRONT_SALES, FRONT, 100, "tax.d2"), "day 2 sale 100")
_advance_to(fixture, 2880)
var funds2 := _day_funds(runtime, 2)
_expect(int(funds2.get(&"rank_tax", -1)) == 10 and int(funds2.get(&"rank_tax_paid", -1)) == 30 and int(funds2.get(&"rank_tax_arrears", -1)) == 0, "day 2 pays the older 20 and the new 10")
_expect(economy.get_balance(FRONT) == 70 and economy.get_balance(FinancePolicy.RANK_TAX_PAYABLE) == 0, "front 70, no tax payable left")
var saved: Dictionary = runtime.create_snapshot()
_expect(runtime.restore_snapshot(saved) and var_to_bytes(saved) == var_to_bytes(runtime.create_snapshot()), "the arrears days restore exactly")
_free_fixture(fixture)
# F7: the ordinary ledger panel reads both funds, the rank tax, the interest, the
# top-ups and their arrears from the same ledger and daily closes.
func _check_ledger_panel(runtime: Variant, clock: Variant) -> void:
var panel := OperatingLedgerPanel.new()
root.add_child(panel)
panel.setup(runtime, clock)
var report: Dictionary = panel.get_report()
_expect(int(report.get(&"cash", -1)) == runtime.economy_service.get_balance(FRONT)
and int(report.get(&"back_cash", -1)) == runtime.economy_service.get_balance(BACK), "the panel reports both funds")
_expect(int(report.get(&"finance_arrears", -1)) == 0, "the panel reports no tax or interest arrears after day 4")
var cash_text: String = panel._cash_label.text
_expect(cash_text.contains("裏金 %dG" % runtime.economy_service.get_balance(BACK)) and cash_text.contains("本日の税") and cash_text.contains("利息"), "the cash section shows the hidden fund, tax and interest")
panel.select_latest_close()
var close_text: String = panel._closed_label.text
_expect(close_text.contains("うち表の売上 100G / 裏の売上 0G") and close_text.contains("世界樹ランク税 10G(支払 10G)") \
and close_text.contains("利息 10G(支払 20G)") and close_text.contains("未払い 税 0G・利息 0G"), "the day 4 close shows sales by fund, tax, interest and arrears")
panel.queue_free()
# F7 through ordinary operation: after a real 00:00 close on the game's own
# Runtime and clock, Main's 帳簿 navigation button opens the ledger.
func _test_ledger_opened_from_main() -> void:
var runtime: Variant = root.get_node("SimulationRuntime")
var clock: Variant = root.get_node("GameClock")
var state: Variant = root.get_node("GameState")
clock.reset(1, 6, 0)
state.reset()
runtime.initialize(clock, state, 5028)
clock.set_paused(true)
root.size = Vector2i(1440, 900)
var main: Control = load("res://scenes/main/main.tscn").instantiate()
root.add_child(main)
await process_frame
runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
for account: StringName in [FRONT_SALES, BACK_SALES, PAYER]:
runtime.economy_service.register_account(account)
runtime.economy_service.register_account(LOAN, true)
_expect(runtime.economy_service.initialize_account(PAYER, 100000, clock.absolute_minute, &"p05.fixture.payer").is_success(), "game Runtime fixture payer funded")
clock.advance_minutes(60)
var fixture := {&"clock": clock, &"state": state, &"runtime": runtime}
_expect(_move(fixture, LOAN, FRONT, 1000, &"LOAN_PRINCIPAL_BORROWED", "ui.borrow") and _sale(fixture, FRONT_SALES, FRONT, 300, "ui.front")
and _sale(fixture, BACK_SALES, BACK, 500, "ui.back"), "day 1 loan and sales on the game Runtime")
clock.advance_minutes(1440 - int(clock.absolute_minute))
_expect(not runtime.operations_coordinator.get_day_summary(1).is_empty(), "the normal clock closes day 1 at 00:00")
var ledger_button: Button = (main.get("navigation_buttons") as Dictionary)[&"ledger"]
ledger_button.pressed.emit()
var panel: OperatingLedgerPanel = main.get("operating_ledger_panel")
_expect(panel.visible and ledger_button.button_pressed, "the 帳簿 button opens the ledger")
var report: Dictionary = panel.get_report()
_expect(int(report.get(&"cash", -1)) == 6210 and int(report.get(&"back_cash", -1)) == 500, "the opened ledger reports front 6210 and hidden 500")
# The day 1 tax and interest are paid at the 00:00 that opens day 2.
var cash_text: String = panel._cash_label.text
_expect(cash_text.contains("裏金 500G(本日の入金 +0G / 表金へ補填 −0G)") and cash_text.contains("本日の税 80G / 利息 10G(未払い 0G)"), "the opened ledger shows the hidden fund, today's tax and interest")
var close_text: String = panel._closed_label.text
_expect(close_text.contains("うち表の売上 300G / 裏の売上 500G") and close_text.contains("世界樹ランク税 80G(支払 80G)/ 利息 10G(支払 10G)")
and close_text.contains("裏金から表金へ補填 0G / 未払い 税 0G・利息 0G"), "the opened ledger shows the day 1 close by fund with tax and interest")
var cleanup: Dictionary = await FixtureCleanup.free_owner_and_wait(self, main)
_expect(bool(cleanup.get(&"passed", false)), "ordinary Main release with audio cleanup")