P05-02: 全費目と資金区分:表金・裏金、維持費未払い、仕入れ、人件費、元本、利息、税の帳簿への接続(原 P05-02 の完了の条件)
This commit is contained in:
@@ -15,6 +15,8 @@ const STARTER_MANA_OUTPUT := 100
|
|||||||
const STARTER_MANA_SOURCE := &"world_tree.mana_source"
|
const STARTER_MANA_SOURCE := &"world_tree.mana_source"
|
||||||
const STARTER_MANA_BUS := &"world_tree.mana_bus"
|
const STARTER_MANA_BUS := &"world_tree.mana_bus"
|
||||||
const PLAYER_CASH_ACCOUNT := &"PLAYER_CASH"
|
const PLAYER_CASH_ACCOUNT := &"PLAYER_CASH"
|
||||||
|
## The settlement.funds report definition (see _daily_fund_report).
|
||||||
|
const FUND_REPORT_VERSION := 2
|
||||||
const CONSTRUCTION_EXPENSE_ACCOUNT := &"CONSTRUCTION_EXPENSE"
|
const CONSTRUCTION_EXPENSE_ACCOUNT := &"CONSTRUCTION_EXPENSE"
|
||||||
const OPERATING_EXPENSE_ACCOUNT := &"OPERATING_EXPENSE"
|
const OPERATING_EXPENSE_ACCOUNT := &"OPERATING_EXPENSE"
|
||||||
const AUTHORITY_FINE_ACCOUNT := &"AUTHORITY_FINE_EXPENSE"
|
const AUTHORITY_FINE_ACCOUNT := &"AUTHORITY_FINE_EXPENSE"
|
||||||
@@ -3604,8 +3606,18 @@ func _operations_payroll_crosslinks_are_valid(
|
|||||||
elif upkeep_debt != 0:
|
elif upkeep_debt != 0:
|
||||||
return false
|
return false
|
||||||
var finance_arrears := 0
|
var finance_arrears := 0
|
||||||
|
# A saved report is checked by the definition it was written with: without
|
||||||
|
# the mark it is version 1; any other mark is not a report this game wrote.
|
||||||
|
var fund_version := FUND_REPORT_VERSION
|
||||||
|
if summary_settlement.get(&"funds", null) is Dictionary:
|
||||||
|
var saved_funds: Dictionary = summary_settlement[&"funds"]
|
||||||
|
var mark: Variant = saved_funds.get(&"fund_report_version", saved_funds.get("fund_report_version", null))
|
||||||
|
if mark == null:
|
||||||
|
fund_version = 1
|
||||||
|
elif typeof(mark) != TYPE_INT or int(mark) != FUND_REPORT_VERSION:
|
||||||
|
return false
|
||||||
var expected_funds := _daily_fund_report(economy, day, coordinator.get_financial_period_start(day),
|
var expected_funds := _daily_fund_report(economy, day, coordinator.get_financial_period_start(day),
|
||||||
coordinator.get_daily_close_minute(day))
|
coordinator.get_daily_close_minute(day), fund_version)
|
||||||
if expected_funds.is_empty():
|
if expected_funds.is_empty():
|
||||||
return false
|
return false
|
||||||
if summary_settlement.has(&"funds"):
|
if summary_settlement.has(&"funds"):
|
||||||
@@ -3776,9 +3788,15 @@ func _post_daily_finance_charge(day: int, boundary_minute: int, new_amount: int,
|
|||||||
|
|
||||||
|
|
||||||
## The fund side of a closed day, derived only from the ledger, so the live close
|
## The fund side of a closed day, derived only from the ledger, so the live close
|
||||||
## and a restored save produce the same report.
|
## and a restored save produce the same report. Version 2 (marked in the report):
|
||||||
func _daily_fund_report(economy: EconomyService, day: int, period_start: int, boundary_minute: int) -> Dictionary:
|
## back_top_up is every top-up of the business day, the operation-time purchases
|
||||||
if economy == null or day <= 0 or period_start < 0 or boundary_minute <= period_start:
|
## and repayments and the legacy 00:00 upkeep as well as the close's own. A report
|
||||||
|
## without the mark is version 1, written while back_top_up counted only the
|
||||||
|
## close's own top-up; a restore checks it by that definition, not a looser one.
|
||||||
|
func _daily_fund_report(economy: EconomyService, day: int, period_start: int, boundary_minute: int,
|
||||||
|
version := FUND_REPORT_VERSION) -> Dictionary:
|
||||||
|
if economy == null or day <= 0 or period_start < 0 or boundary_minute <= period_start \
|
||||||
|
or version not in [1, FUND_REPORT_VERSION]:
|
||||||
return {}
|
return {}
|
||||||
var report := {
|
var report := {
|
||||||
&"revenue_front": 0, &"revenue_back": 0, &"back_top_up": 0,
|
&"revenue_front": 0, &"revenue_back": 0, &"back_top_up": 0,
|
||||||
@@ -3811,6 +3829,19 @@ func _daily_fund_report(economy: EconomyService, day: int, period_start: int, bo
|
|||||||
report[StringName(String(field) + "_paid")] = int(report[StringName(String(field) + "_paid")]) + entry.amount
|
report[StringName(String(field) + "_paid")] = int(report[StringName(String(field) + "_paid")]) + entry.amount
|
||||||
else:
|
else:
|
||||||
return {}
|
return {}
|
||||||
|
if version == FUND_REPORT_VERSION:
|
||||||
|
# The closes are keyed; the previous day's close is posted at period_start.
|
||||||
|
var closes := {}
|
||||||
|
for close_day: int in [day - 1, day]:
|
||||||
|
var close := economy.get_transaction_id_for_idempotency_key(StringName("finance.back_top_up.day.%08d" % close_day))
|
||||||
|
if not close.is_empty():
|
||||||
|
closes[close] = true
|
||||||
|
for entry: LedgerEntry in economy.get_entries():
|
||||||
|
if entry.category == FinancePolicy.TOP_UP_CATEGORY and entry.absolute_minute >= period_start \
|
||||||
|
and entry.absolute_minute < boundary_minute and not closes.has(entry.transaction_id):
|
||||||
|
report[&"back_top_up"] = int(report[&"back_top_up"]) + entry.amount
|
||||||
|
postings = true
|
||||||
|
report[&"fund_report_version"] = FUND_REPORT_VERSION
|
||||||
report[&"rank_tax_arrears"] = _payable_arrears_through(economy, FinancePolicy.RANK_TAX_PAYABLE, boundary_minute)
|
report[&"rank_tax_arrears"] = _payable_arrears_through(economy, FinancePolicy.RANK_TAX_PAYABLE, boundary_minute)
|
||||||
report[&"loan_interest_arrears"] = _payable_arrears_through(economy, FinancePolicy.LOAN_INTEREST_PAYABLE, boundary_minute)
|
report[&"loan_interest_arrears"] = _payable_arrears_through(economy, FinancePolicy.LOAN_INTEREST_PAYABLE, boundary_minute)
|
||||||
report[&"has_finance_postings"] = postings
|
report[&"has_finance_postings"] = postings
|
||||||
@@ -12155,6 +12186,9 @@ func _apply_service_transaction(
|
|||||||
if service.price_snapshot > 0:
|
if service.price_snapshot > 0:
|
||||||
var payer_account := _guest_wallet_account(request.guest_ids[0])
|
var payer_account := _guest_wallet_account(request.guest_ids[0])
|
||||||
var revenue_account := _facility_revenue_account(facility.instance_id)
|
var revenue_account := _facility_revenue_account(facility.instance_id)
|
||||||
|
# Only a registered definition with a hidden-service tag routes the sale to the
|
||||||
|
# hidden fund. A facility whose definition cannot be read keeps the existing
|
||||||
|
# front-fund behaviour (disclosed in execution/p05-02/REPAIR_02.md).
|
||||||
var sale_definition := definition_registry.get_facility(facility.definition_id)
|
var sale_definition := definition_registry.get_facility(facility.definition_id)
|
||||||
var sale_fund := FinancePolicy.BACK_CASH \
|
var sale_fund := FinancePolicy.BACK_CASH \
|
||||||
if sale_definition != null and FinancePolicy.is_back_fund_service(sale_definition.tags) else PLAYER_CASH_ACCOUNT
|
if sale_definition != null and FinancePolicy.is_back_fund_service(sale_definition.tags) else PLAYER_CASH_ACCOUNT
|
||||||
|
|||||||
+1
-1
@@ -3931,7 +3931,7 @@
|
|||||||
"template": "PASS: <checks> checks",
|
"template": "PASS: <checks> checks",
|
||||||
"required": true,
|
"required": true,
|
||||||
"fixed_check_count_used": false,
|
"fixed_check_count_used": false,
|
||||||
"source_sha256_at_schema_audit": "38a90af176e88e83041af7b20324f1383f9f38cdf05fa452c6cf75b8e3bf5b95",
|
"source_sha256_at_schema_audit": "0dcef5fb731753e1a8f4269b445aa0df68e5428431e3422712c27056008edeba",
|
||||||
"authority": {
|
"authority": {
|
||||||
"path": "docs/plans/original-completion-2026-10-02/execution/p05-02/SPEC.md",
|
"path": "docs/plans/original-completion-2026-10-02/execution/p05-02/SPEC.md",
|
||||||
"scope": "new P05-02 suite marker; native execution pending"
|
"scope": "new P05-02 suite marker; native execution pending"
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# P05-02 修正2: 実際の経路での表裏の資金・補填・利息・税の滞納と、日次の補填の定義
|
||||||
|
|
||||||
|
- S `37deb6e877e60e8f19ae4fbc64382880cf7f75bb` の whole-unit の独立レビュー(r02)は changes_requested(指摘 1 high・2 medium・3 low)。正規の受付で `repair_pending`、commander の修正2(`COMMANDER_REPAIR_PLAN_P05_02_R02.md`、訂正の追記 `..._CORRECTION_01.md`)。S `37deb6e` までの native・全 roster・validation・review の記録と失敗は全て保持し、新しい S に流用しない。原本は 23/88 のまま。
|
||||||
|
|
||||||
|
## 試験(`tests/economy/test_p05_funds_runtime.gd` に足した場面)
|
||||||
|
|
||||||
|
- 指摘1(F1・F5)`_test_actual_service_funds`: Runtime で、表の施設(`massage_room`、通常の雇用 `staff_petra` を DAY の配置)と、登録済みの裏の札の施設(`adult_private_room`、地下。rank 2・掘削・客用の通路と階段・職員用の通路と階段を通常の command で作る)を建てて開ける。自然に来る客が自分でサービスを選び、地下の個室へは地上から歩いて降りて、完了させる(客を置く fixture は使わない)。00:00 の締めで、massage_room の `SERVICE_CASH_SWEEP` は全て `PLAYER_CASH`、adult_private_room の分は全て `BACK_CASH`(他の行き先 0)、個室の支払が自然に来た客の財布からであること、`funds.revenue_front`・`revenue_back` がその日の行き先ごとの合計と一致すること、売上が表+裏であること、世界樹ランク税が両方の売上の合計の 10%(切り捨て)で払われたこと、裏金の残高と増減がその日の裏の売上と一致することを確かめる。行き先の選択を元に戻すとこの場面は失敗する。
|
||||||
|
- 指摘2(F2):
|
||||||
|
- 人件費: 同じ場面の2日目、締めの前に表金を 5 に下げ(台帳の fixture。下げるだけ)、本物の締めが実際の雇用の給与・維持費・税を払う。`funds.back_top_up` が(給与+維持費+税−5)、給与の未払い 0、表金 0、裏金がその分減ることを確かめる。
|
||||||
|
- 自動の仕入れ `_test_operation_top_ups_and_day_total`: 倉庫に自動の仕入れ(10 個)を設定し、表金 2・裏金 1,000 で時計を進める。`advance_auto_purchases` の本物の注文が 30 で買い、領収(`SupplyAutoPurchasePlans.receipt_from_ledger`)が検証され、28 が裏金からの補填(鍵 `finance.back_top_up.purchase.<注文の鍵>`)であることを確かめる。
|
||||||
|
- 旧暦の 00:00 の維持費 `_test_legacy_midnight_top_up`: `tests/mvp/test_upkeep_legacy_midnight.gd` と同じ作り方の旧暦の save(締めが 06:00)を読み、表金 0・裏金 500 で 00:00 を越える。00:00 の維持費 1 が裏金から補填され(鍵 `finance.back_top_up.midnight.1440`)、滞納 0、06:00 の締めの report がその補填を数えることを確かめる。
|
||||||
|
- 指摘3(F4): 同じ `_test_operation_top_ups_and_day_total` で、1,000 を借り(台帳の fixture。借入の発生源は無い)、返済の command(`repay_loan_principal`)で 200 を返す(表金 0 なので裏金で払う)。その日の締めの利息が返済の後の残高 800 の 1% の 8 で、払われることを確かめる。
|
||||||
|
- 指摘4(F3)`_test_tax_arrears_next_day`: 売上 200 の日に表裏の資金が無く、税 20 が未払いのまま繰り越す(日次の滞納と Operations の report に出る)。翌日の締めで古い 20 と新しい 10 を払い、税の未払いが 0 になることを確かめる。
|
||||||
|
- 各場面の最後に save の写しから戻し、そのまま一致することを確かめる。
|
||||||
|
|
||||||
|
## 日次の補填の定義(指摘5)と、保存済みの report の互換
|
||||||
|
|
||||||
|
- `funds.back_top_up` は、その営業日に記帳された全ての補填(操作の時の仕入れ・自動の仕入れ・返済、旧暦の 00:00 の維持費、締めの補填)の合計にした(`autoload/simulation_runtime.gd` の `_daily_fund_report`)。営業日の区切りは売上の集計と同じ(period_start 以上・締めの分より前)。締めの補填は鍵で数え、前日の締めの補填(period_start の分に記帳)は数えない。帳簿の画面の締めの欄「裏金から表金へ補填」はこの値を出す。
|
||||||
|
- report に定義の版 `fund_report_version: 2` を入れた。版の無い report は、S `7808ca7`・`37deb6e`(統合されていない)で保存された、締めの補填だけを数えた定義(版1)。読込の照合(`settlement.funds` と台帳から作り直した report の比較)は、保存された report が書かれた定義で比べる。版の無い report は版1の式でだけ受け、新しい式の値を持つ版の無い report、版2の印で締めの分だけの値の report、知らない版の report は拒む(緩めない)。A12(資金を分ける前の save)は report を持たないので、今までどおり(帳簿の記帳が無いことを確かめる)。版1の report は書かれた時のまま残し、画面は「(締めの時の分のみ)」と出す。
|
||||||
|
- 試験: 同じ日に注文・返済・締めの3つの補填がある save から、版1の形の report(印を外し、締めの分の値)を作って読めること、その値と表示が残ること、上の3つの不正な形が拒まれ何も変えないことを確かめる。
|
||||||
|
|
||||||
|
## 開示(指摘6 と、修正2で分かったこと)
|
||||||
|
|
||||||
|
- 今のゲームで裏金に入る売上の発生源は、登録済みの施設では `adult_private_room` だけ。`black_market`(trade。原子要件 income_trade のユニット売却に当たる)と `vip_private_salon` は拡張の定義で既定では登録されない。カジノ・脅迫金・ユニット売却の発生源は無い。原子要件 income_casino・income_blackmail・income_trade は未達のままで、この開示で完了にはしない。
|
||||||
|
- 定義が引けない施設の売上は、既存の挙動どおり表金に入る(裏の札の判定ができないため。`_apply_service_transaction` の注記)。定義の無い施設を扱う既存の経路は変えない。
|
||||||
|
- F1 の実際の売上の経路の試験で、`adult_private_room` の担当(役割 `service_attendant`)は Runtime の `register_staff` の command で登録した職員(fixture の職員。給与 0 の FREE_HIRE)。通常の雇用の候補にこの役割は無く、`_staff_role_matches_facility` にも代わりの役割は無いので、通常の Main(雇用と配置の画面)ではこの施設に職員を置けず、裏の売上を得られない。これは原 work の P09・P11 の `current_paid_role_compatibility_checkpoint.remaining_paid_role`(「adult_private_room/service_attendant has no canonical paid hire candidate」、2026-10-08、`work_packages.json`)に残っている原残件で、通常の Main での全施設の営業は未達。P05-02 では職員の候補・役割・給与・定義を作らず、P09・P11 の残件はこの開示で閉じない。
|
||||||
|
- `adult_private_room` の定義の `unlock_rank` 3・`research_requirements`(discreet_hospitality)・`opening_schedule`(夜だけ)は、今の Runtime では建設や営業の条件として使われていない(定義の検査だけ)。試験は rank 2(地下の開放)で建て、夜に営業させた。P05-02 では変えない。
|
||||||
|
- 補填の元にした表金の減少(台帳の fixture)と借入(台帳の fixture)は、残高を作るだけで、確かめる支払・補填・利息・税は本物の経路。
|
||||||
|
|
||||||
|
## 変えていないもの
|
||||||
|
|
||||||
|
- 登録の argv、`tools/run_p05_funds_tests.py`、狭い roster(8 suites+再開の2段、実 10 段)、試験の既存の期待値(`EXPECTATION_CHANGES.json` に足すものは無い)。
|
||||||
@@ -164,7 +164,9 @@ func refresh() -> void:
|
|||||||
_closed_label.text = "売上 %dG / 返金 %dG\n運営費(仕入れ・契約金等) %dG\n給与 %dG / その他 %dG\n日次利益 %+dG\n前日比 %+dG / 未払給与 %dG\n帳簿照合:%s" % [totals.get(&"revenue", 0), totals.get(&"refunds", 0), totals.get(&"cost_of_goods", 0), totals.get(&"wages", 0), other, summary.get(&"net_profit", 0), summary.get(&"previous_day_delta", 0), summary.get(&"arrears_total", 0), "一致" if bool(summary.get(&"balanced", false)) else "差額あり"]
|
_closed_label.text = "売上 %dG / 返金 %dG\n運営費(仕入れ・契約金等) %dG\n給与 %dG / その他 %dG\n日次利益 %+dG\n前日比 %+dG / 未払給与 %dG\n帳簿照合:%s" % [totals.get(&"revenue", 0), totals.get(&"refunds", 0), totals.get(&"cost_of_goods", 0), totals.get(&"wages", 0), other, summary.get(&"net_profit", 0), summary.get(&"previous_day_delta", 0), summary.get(&"arrears_total", 0), "一致" if bool(summary.get(&"balanced", false)) else "差額あり"]
|
||||||
var funds: Dictionary = (summary.get(&"settlement", {}) as Dictionary).get(&"funds", {})
|
var funds: Dictionary = (summary.get(&"settlement", {}) as Dictionary).get(&"funds", {})
|
||||||
if not funds.is_empty():
|
if not funds.is_empty():
|
||||||
_closed_label.text += "\nうち表の売上 %dG / 裏の売上 %dG\n世界樹ランク税 %dG(支払 %dG)/ 利息 %dG(支払 %dG)\n裏金から表金へ補填 %dG / 未払い 税 %dG・利息 %dG" % [int(funds.get(&"revenue_front", 0)), int(funds.get(&"revenue_back", 0)), int(funds.get(&"rank_tax", 0)), int(funds.get(&"rank_tax_paid", 0)), int(funds.get(&"loan_interest", 0)), int(funds.get(&"loan_interest_paid", 0)), int(funds.get(&"back_top_up", 0)), int(funds.get(&"rank_tax_arrears", 0)), int(funds.get(&"loan_interest_arrears", 0))]
|
# The day's every top-up; a report saved before the version mark counted the close's own only.
|
||||||
|
var top_up_scope := "" if funds.has(&"fund_report_version") or funds.has("fund_report_version") else "(締めの時の分のみ)"
|
||||||
|
_closed_label.text += "\nうち表の売上 %dG / 裏の売上 %dG\n世界樹ランク税 %dG(支払 %dG)/ 利息 %dG(支払 %dG)\n裏金から表金へ補填%s %dG / 未払い 税 %dG・利息 %dG" % [int(funds.get(&"revenue_front", 0)), int(funds.get(&"revenue_back", 0)), int(funds.get(&"rank_tax", 0)), int(funds.get(&"rank_tax_paid", 0)), int(funds.get(&"loan_interest", 0)), int(funds.get(&"loan_interest_paid", 0)), top_up_scope, int(funds.get(&"back_top_up", 0)), int(funds.get(&"rank_tax_arrears", 0)), int(funds.get(&"loan_interest_arrears", 0))]
|
||||||
if totals.has(&"financing_principal_inflows") or totals.has(&"financing_principal_outflows"):
|
if totals.has(&"financing_principal_inflows") or totals.has(&"financing_principal_outflows"):
|
||||||
_closed_label.text += "\n借入元本の入金 %dG(売上外) / 元本返済 %dG(決算経費)" % [int(totals.get(&"financing_principal_inflows", 0)), int(totals.get(&"financing_principal_outflows", 0))]
|
_closed_label.text += "\n借入元本の入金 %dG(売上外) / 元本返済 %dG(決算経費)" % [int(totals.get(&"financing_principal_inflows", 0)), int(totals.get(&"financing_principal_outflows", 0))]
|
||||||
if _runtime.operations_coordinator.is_legacy_principal_profit_history(int(summary[&"day"])):
|
if _runtime.operations_coordinator.is_legacy_principal_profit_history(int(summary[&"day"])):
|
||||||
|
|||||||
@@ -3,10 +3,14 @@ const FixtureCleanup := preload("res://tests/helpers/ui_audio_fixture_cleanup.gd
|
|||||||
|
|
||||||
## P05-02: front (表金) and hidden (裏金) funds, the A09 payment rule, the A10 rank
|
## 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.
|
## tax, the A11 loan interest, the three business day reconciliation and saves.
|
||||||
## Sales, debt and drains are explicit ledger fixtures; the daily closes are the
|
## The policy and reconciliation scenes use explicit ledger fixtures for sales,
|
||||||
## production Operations boundaries. The last check opens the ledger from the
|
## debt and drains, with the production Operations closes. The repair 2 scenes
|
||||||
## game's own Main (F7), so the runner gives this suite the ordinary Main
|
## run the production paths themselves: built and staffed facilities whose guests
|
||||||
## audio cleanup proof (tools/regression/suite-policy.json).
|
## 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 FRONT := &"PLAYER_CASH"
|
||||||
const BACK := &"BACK_CASH"
|
const BACK := &"BACK_CASH"
|
||||||
@@ -34,6 +38,10 @@ func _run() -> void:
|
|||||||
_test_repayment_top_up()
|
_test_repayment_top_up()
|
||||||
_test_purchase_top_up()
|
_test_purchase_top_up()
|
||||||
_test_upkeep_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 _test_ledger_opened_from_main()
|
||||||
await process_frame
|
await process_frame
|
||||||
if _failures == 0:
|
if _failures == 0:
|
||||||
@@ -402,6 +410,289 @@ func _test_upkeep_top_up() -> void:
|
|||||||
_free_fixture(fixture)
|
_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
|
# 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.
|
# top-ups and their arrears from the same ledger and daily closes.
|
||||||
func _check_ledger_panel(runtime: Variant, clock: Variant) -> void:
|
func _check_ledger_panel(runtime: Variant, clock: Variant) -> void:
|
||||||
|
|||||||
@@ -416,7 +416,7 @@
|
|||||||
"new_audio_QA": false
|
"new_audio_QA": false
|
||||||
},
|
},
|
||||||
"tests/economy/test_p05_funds_runtime.gd": {
|
"tests/economy/test_p05_funds_runtime.gd": {
|
||||||
"source_sha256": "38a90af176e88e83041af7b20324f1383f9f38cdf05fa452c6cf75b8e3bf5b95",
|
"source_sha256": "0dcef5fb731753e1a8f4269b445aa0df68e5428431e3422712c27056008edeba",
|
||||||
"ordinary_cleanup_prepared": true,
|
"ordinary_cleanup_prepared": true,
|
||||||
"ordinary_control_labels": [
|
"ordinary_control_labels": [
|
||||||
"single"
|
"single"
|
||||||
|
|||||||
Reference in New Issue
Block a user