Files
TheTree_mock/scripts/economy/facility_upkeep_settlement.gd
Takeshi 5dde0c5ec2 Record original-alignment implementation and traceability checkpoint
Capture current source, content data, tests, asset licenses, and original-specification traceability. Original implementation remains incomplete. The accepted matching UI generation passed 154 suites and 12,253 checks, with eight rendered staff-contract screens reviewed. Latest native Windows and full-original acceptance remain outstanding.
2026-09-30 23:52:24 +09:00

253 lines
10 KiB
GDScript

class_name FacilityUpkeepSettlement
extends RefCounted
## Settles exact accrued upkeep against the existing balanced ledger.
## Whole gold is recognized as expense even when cash is insufficient. Sub-gold
## fractions carry forward; arrears survive the removal of the owning facility.
## The caller publishes the returned transaction notification after committing
## its wider Runtime transaction (as with payroll).
const Accrual = preload("res://scripts/economy/facility_upkeep_accrual.gd")
const VERSION := 1
const CASH: StringName = &"PLAYER_CASH"
const EXPENSE: StringName = &"MAINTENANCE_EXPENSE"
const PAYABLE: StringName = &"MAINTENANCE_PAYABLE"
var _receipts: Array[Dictionary] = []
var _arrears: Dictionary = {}
var _fractions: Dictionary = {}
static func prepare_accounts(economy: EconomyService) -> bool:
if economy == null or not economy.has_account(CASH):
return false
# Reject incompatible accounts before registering anything.
if economy.has_account(EXPENSE) and economy.account_allows_negative(EXPENSE):
return false
if economy.has_account(PAYABLE) and not economy.account_allows_negative(PAYABLE):
return false
if not economy.has_account(EXPENSE):
economy.register_account(EXPENSE)
if not economy.has_account(PAYABLE):
economy.register_account(PAYABLE, true)
return true
func settle(period: Dictionary, economy: EconomyService) -> Dictionary:
var canonical := _parse_period(period)
if canonical.is_empty():
return _failure(&"INVALID_PERIOD")
for saved: Dictionary in _receipts:
if saved.period.period_end == canonical.period_end:
if saved.period != canonical or not validate_ledger(economy):
return _failure(&"REPLAY_MISMATCH")
return {&"ok": true, &"replayed": true, &"receipt": saved.duplicate(true), &"transaction_id": &""}
if not _receipts.is_empty() and canonical.period_start != _receipts[-1].period.period_end:
return _failure(&"PERIOD_GAP_OR_REWIND")
if _receipts.size() >= Accrual.MAX_PERIODS:
return _failure(&"TOO_MANY_PERIODS")
if not validate_ledger(economy):
return _failure(&"LEDGER_MISMATCH")
var plan := _plan(canonical, economy.get_balance(CASH))
if not plan[&"ok"]:
return plan
var entries: Array = _entries(canonical, plan.rows)
var transaction_id: StringName = &""
if not entries.is_empty():
var result := economy.post(entries, StringName("upkeep.period.%d" % int(canonical.period_end)), &"", false)
if not result.is_success():
return _failure(&"LEDGER_POST_FAILED")
transaction_id = StringName(result.details[&"transaction_id"])
# No fallible mutation follows the single, atomic ledger post.
_arrears = plan.arrears
_fractions = plan.fractions
var receipt := {&"period": canonical, &"rows": plan.rows, &"transaction_id": String(transaction_id)}
_receipts.append(receipt)
return {&"ok": true, &"replayed": false, &"receipt": receipt.duplicate(true), &"transaction_id": transaction_id}
func get_arrears_through(minute: int) -> int:
var low := 0
var high := _receipts.size()
while low < high:
var middle := (low + high) / 2
if int(_receipts[middle][&"period"][&"period_end"]) <= minute:
low = middle + 1
else:
high = middle
if low == 0:
return 0
var amount := 0
for row: Dictionary in _receipts[low - 1][&"rows"]:
amount += int(row[&"arrears"])
return amount
func get_facility_balance(facility_id: StringName) -> Dictionary:
var id := String(facility_id)
return {&"arrears": int(_arrears.get(id, 0)), &"fraction_units": int(_fractions.get(id, 0))}
func get_settled_period_count() -> int:
return _receipts.size()
func get_arrears_total() -> int:
var debt := 0
for amount: int in _arrears.values():
debt += amount
return debt
func get_report() -> Dictionary:
return {&"arrears_total": get_arrears_total(), &"arrears_by_facility": _arrears.duplicate(),
&"fraction_units": _fractions.duplicate(), &"gold_denominator": Accrual.MINUTES_PER_DAY,
&"receipts": _receipts.duplicate(true)}
func create_snapshot() -> Dictionary:
# Derived balances are reconstructed, not trusted from the save file.
return {&"version": VERSION, &"receipts": _receipts.duplicate(true)}
func restore_snapshot(source: Dictionary, economy: EconomyService) -> bool:
if not Accrual._exact_keys(source, ["version", "receipts"]) \
or typeof(source["version"]) != TYPE_INT or source["version"] != VERSION \
or not source["receipts"] is Array or source["receipts"].size() > Accrual.MAX_PERIODS:
return false
var candidate := FacilityUpkeepSettlement.new()
for raw: Variant in source["receipts"]:
if not raw is Dictionary or not Accrual._exact_keys(raw, ["period", "rows", "transaction_id"]) \
or not raw["period"] is Dictionary or not raw["rows"] is Array \
or typeof(raw["transaction_id"]) not in [TYPE_STRING, TYPE_STRING_NAME]:
return false
var period := _parse_period(raw["period"])
if period.is_empty() or (not candidate._receipts.is_empty() \
and period.period_start != candidate._receipts[-1].period.period_end):
return false
var budget := 0
for row: Variant in raw["rows"]:
if not row is Dictionary or not Accrual._exact_keys(row, ["facility_id", "new_expense", "paid", "arrears", "fraction_units"]) \
or typeof(row["facility_id"]) not in [TYPE_STRING, TYPE_STRING_NAME]:
return false
for field: String in ["new_expense", "paid", "arrears", "fraction_units"]:
if typeof(row[field]) != TYPE_INT or row[field] < 0 or row[field] > 9000000000000000:
return false
if budget > 9000000000000000 - int(row["paid"]):
return false
budget += int(row["paid"])
var plan := candidate._plan(period, budget)
if not plan[&"ok"] or plan.rows != raw["rows"]:
return false
candidate._arrears = plan.arrears
candidate._fractions = plan.fractions
candidate._receipts.append({&"period": period, &"rows": plan.rows, &"transaction_id": String(raw["transaction_id"])})
if not candidate.validate_ledger(economy):
return false
_receipts = candidate._receipts
_arrears = candidate._arrears
_fractions = candidate._fractions
return true
func validate_ledger(economy: EconomyService) -> bool:
if economy == null or not economy.has_account(CASH) or not economy.has_account(EXPENSE) \
or not economy.has_account(PAYABLE) or economy.account_allows_negative(EXPENSE) \
or not economy.account_allows_negative(PAYABLE):
return false
var expected_expense := 0
var expected_debt := 0
var owned_transactions: Dictionary = {}
for receipt: Dictionary in _receipts:
var expected: Array = _entries(receipt.period, receipt.rows)
var transaction := StringName(receipt.transaction_id)
if economy.get_transaction_id_for_idempotency_key(StringName("upkeep.period.%d" % int(receipt.period.period_end))) != transaction:
return false
if expected.is_empty():
if not transaction.is_empty():
return false
continue
if transaction.is_empty() or owned_transactions.has(transaction):
return false
owned_transactions[transaction] = true
var actual := economy.get_transaction_entries(transaction)
if actual.size() != expected.size():
return false
for i in range(expected.size()):
var a: LedgerEntry = actual[i]
var e: LedgerEntry = expected[i]
if a.amount != e.amount or a.absolute_minute != e.absolute_minute or a.category != e.category \
or a.debit_account != e.debit_account or a.credit_account != e.credit_account \
or a.source_entity_id != e.source_entity_id or a.memo_key != e.memo_key \
or not a.related_transaction_id.is_empty():
return false
for row: Dictionary in receipt.rows:
expected_expense += int(row.new_expense)
for value: int in _arrears.values():
expected_debt += value
for entry: LedgerEntry in economy.get_entries():
if entry.debit_account in [EXPENSE, PAYABLE] or entry.credit_account in [EXPENSE, PAYABLE]:
if not owned_transactions.has(entry.transaction_id):
return false
return economy.get_balance(EXPENSE) == expected_expense and economy.get_balance(PAYABLE) == -expected_debt
func _plan(period: Dictionary, cash: int) -> Dictionary:
if cash < 0:
return _failure(&"INVALID_CASH")
var ids: Dictionary = {}
for source: Dictionary in [_arrears, _fractions, period.facility_units]:
for id: String in source:
ids[id] = true
if ids.size() > Accrual.MAX_FACILITIES:
return _failure(&"TOO_MANY_FACILITIES")
var ordered := ids.keys()
ordered.sort()
var rows: Array[Dictionary] = []
var arrears: Dictionary = {}
var fractions: Dictionary = {}
for id: String in ordered:
var units := int(period.facility_units.get(id, 0)) + int(_fractions.get(id, 0))
var expense := units / Accrual.MINUTES_PER_DAY
var fraction := units % Accrual.MINUTES_PER_DAY
var debt := int(_arrears.get(id, 0)) + expense
var paid := mini(debt, cash)
cash -= paid
if debt > paid:
arrears[id] = debt - paid
if fraction > 0:
fractions[id] = fraction
rows.append({&"facility_id": id, &"new_expense": expense, &"paid": paid,
&"arrears": debt - paid, &"fraction_units": fraction})
return {&"ok": true, &"rows": rows, &"arrears": arrears, &"fractions": fractions}
static func _parse_period(source: Dictionary) -> Dictionary:
if not Accrual._exact_keys(source, ["period_start", "period_end", "facility_units"]) \
or typeof(source["period_start"]) != TYPE_INT or typeof(source["period_end"]) != TYPE_INT:
return {}
var start := int(source["period_start"])
var end := int(source["period_end"])
if start < 0 or end > Accrual.MAX_MINUTE or end != (start / 1440 + 1) * 1440:
return {}
var units := Accrual._parse_units(source["facility_units"], Accrual.MAX_DAILY_RATE * (end - start), false)
if not units[&"ok"]:
return {}
return {&"period_start": start, &"period_end": end, &"facility_units": units[&"value"]}
static func _entries(period: Dictionary, rows: Array) -> Array:
var entries: Array = []
for row: Dictionary in rows:
var id := StringName(row.facility_id)
if int(row.new_expense) > 0:
entries.append(LedgerEntry.new().configure(&"", int(period.period_end) - 1,
EXPENSE, PAYABLE, int(row.new_expense), &"MAINTENANCE_ACCRUAL", id, &"", &"facility.upkeep.accrued"))
if int(row.paid) > 0:
entries.append(LedgerEntry.new().configure(&"", int(period.period_end),
PAYABLE, CASH, int(row.paid), &"MAINTENANCE_PAYMENT", id, &"", &"facility.upkeep.paid"))
return entries
static func _failure(reason: StringName) -> Dictionary:
return {&"ok": false, &"reason": reason}