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.
253 lines
10 KiB
GDScript
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}
|