extends SceneTree const LedgerEntryType = preload("res://scripts/economy/ledger_entry.gd") const EconomyServiceType = preload("res://scripts/economy/economy_service.gd") const CommandResultType = preload("res://scripts/core/command_result.gd") var _checks := 0 var _failures := 0 func _initialize() -> void: call_deferred("_run") func _run() -> void: print("S2: Economy Service") _test_initialization_and_insufficient_balance() _test_idempotency_posts_once() _test_reverse_and_partial_refund() _test_compound_post_is_atomic() _test_daily_aggregation_and_reconciliation() _test_snapshot_round_trip_is_atomic() if _failures == 0: print("PASS: %d checks" % _checks) else: push_error("FAIL: %d of %d checks failed" % [_failures, _checks]) quit(_failures) func _test_initialization_and_insufficient_balance() -> void: var economy := _economy() var initialized = economy.initialize_account(&"PLAYER_CASH", 1_000, 360, &"init-player") _expect(initialized.is_success(), "initial Gold is posted through SYSTEM_SOURCE") _expect(economy.get_balance(&"PLAYER_CASH") == 1_000, "initialization debits player cash") _expect(economy.get_balance(EconomyServiceType.SYSTEM_SOURCE) == -1_000, "SYSTEM_SOURCE is the explicit counter-account") var entry = economy.get_transaction_entries(initialized.details[&"transaction_id"])[0] _expect(entry.amount == 1_000 and entry.category == &"INITIALIZATION", "initialization creates a positive integer ledger entry") var before := economy.create_snapshot() var rejected = economy.transfer( &"PLAYER_CASH", &"GUEST_WALLET:guest_1", 1_001, 361, &"TEST", &"", &"too-much" ) _expect(not rejected.is_success(), "an account without credit cannot overspend") _expect(rejected.code == CommandResultType.Code.VALIDATION_FAILED, "insufficient Gold is a validation failure") _expect(economy.create_snapshot() == before, "rejected transfer changes neither ledger nor balances") _expect(not economy.register_account(&"PLAYER_CASH"), "an account definition cannot be replaced to bypass its policy") func _test_idempotency_posts_once() -> void: var economy := _economy() economy.initialize_account(&"PLAYER_CASH", 500, 360, &"init") var first = economy.transfer( &"PLAYER_CASH", &"GUEST_WALLET:guest_1", 125, 400, &"GIFT", &"player", &"gift-once" ) var entry_count := economy.get_entries().size() var duplicate = economy.transfer( &"PLAYER_CASH", &"GUEST_WALLET:guest_1", 125, 400, &"GIFT", &"player", &"gift-once" ) _expect(first.is_success(), "first idempotent command posts") _expect(duplicate.code == CommandResultType.Code.DUPLICATE_IDEMPOTENCY_KEY, "duplicate idempotency key is rejected explicitly") _expect(economy.get_entries().size() == entry_count, "duplicate command appends no second entry") _expect(economy.get_balance(&"PLAYER_CASH") == 375, "sender is credited exactly once") _expect(economy.get_balance(&"GUEST_WALLET:guest_1") == 125, "receiver is debited exactly once") _expect( economy.get_transaction_id_for_idempotency_key(&"gift-once") == first.details[&"transaction_id"], "idempotency index resolves the original transaction" ) func _test_reverse_and_partial_refund() -> void: var economy := _economy() economy.initialize_account(&"GUEST_WALLET:guest_1", 500, 360, &"init-guest") var payment = economy.transfer( &"GUEST_WALLET:guest_1", &"FACILITY_REVENUE:inn", 200, 500, &"SERVICE_SALE", &"inn", &"sale-1" ) var payment_id: StringName = payment.details[&"transaction_id"] var reversed = economy.reverse(payment_id, 510, &"reverse-sale") _expect(reversed.is_success(), "an existing transaction reverses with a new entry") _expect(economy.get_balance(&"GUEST_WALLET:guest_1") == 500, "reversal restores the payer balance") _expect(economy.get_balance(&"FACILITY_REVENUE:inn") == 0, "reversal restores the receiver balance") var reverse_entry = economy.get_transaction_entries(reversed.details[&"transaction_id"])[0] _expect(reverse_entry.related_transaction_id == payment_id, "reversal retains the original transaction reference") _expect(reverse_entry.debit_account == &"GUEST_WALLET:guest_1" and reverse_entry.credit_account == &"FACILITY_REVENUE:inn", "reversal swaps debit and credit") _expect(not economy.reverse(payment_id, 511, &"reverse-again").is_success(), "a transaction cannot be fully reversed twice") var second_payment = economy.transfer( &"GUEST_WALLET:guest_1", &"FACILITY_REVENUE:inn", 120, 520, &"SERVICE_SALE", &"inn", &"sale-2" ) var second_id: StringName = second_payment.details[&"transaction_id"] _expect(economy.refund(second_id, 40, 530, &"refund-1").is_success(), "a one-line transaction supports a partial refund") _expect(economy.refund(second_id, 80, 540, &"refund-2").is_success(), "remaining refundable Gold can be returned") _expect(economy.get_refunded_amount(second_id) == 120, "refund total is derived from related entries") _expect(not economy.refund(second_id, 1, 541, &"refund-over").is_success(), "refunds cannot exceed the source transaction") func _test_compound_post_is_atomic() -> void: var economy := _economy() economy.initialize_account(&"PLAYER_CASH", 400, 360, &"init-player") economy.initialize_account(&"GUEST_WALLET:guest_1", 50, 360, &"init-guest") var before := economy.create_snapshot() var entries: Array = [ _entry(600, &"STAFF_WALLET:staff_1", &"PLAYER_CASH", 100, &"PAYROLL"), _entry(600, &"STAFF_WALLET:staff_1", &"GUEST_WALLET:guest_1", 51, &"PAYROLL"), ] var rejected = economy.post(entries, &"compound-fail") _expect(not rejected.is_success(), "one invalid line rejects a compound transaction") _expect(economy.create_snapshot() == before, "compound failure rolls back every line and index") entries[1].amount = 50 var posted = economy.post(entries, &"compound-ok") _expect(posted.is_success(), "a fully valid compound transaction posts") _expect(economy.get_transaction_entries(posted.details[&"transaction_id"]).size() == 2, "compound lines share one transaction ID") _expect(economy.get_balance(&"STAFF_WALLET:staff_1") == 150, "compound debit deltas commit together") _expect(economy.get_balance(&"PLAYER_CASH") == 300 and economy.get_balance(&"GUEST_WALLET:guest_1") == 0, "compound credits commit together") func _test_daily_aggregation_and_reconciliation() -> void: var economy := _economy() economy.initialize_account(&"PLAYER_CASH", 1_000, 360, &"init") economy.transfer(&"PLAYER_CASH", &"GUEST_WALLET:guest_1", 75, 700, &"GIFT", &"player", &"gift") economy.transfer(&"PLAYER_CASH", &"STAFF_WALLET:staff_1", 125, 1_500, &"WAGE", &"staff_1", &"wage") var day_one := economy.get_daily_summary(1) _expect(day_one[&"transaction_count"] == 2 and day_one[&"balanced"], "day close counts only transactions inside its minute range") _expect(day_one[&"category_totals"][&"INITIALIZATION"] == 1_000, "day close aggregates category amounts") _expect(day_one[&"account_totals"][&"PLAYER_CASH"][&"net"] == 925, "day close aggregates per-account debit, credit and net") _expect(economy.validate_ledger()[&"ok"], "ledger and derived balance cache reconcile") economy._balance_cache[&"PLAYER_CASH"] = 999_999 var mismatch := economy.validate_ledger() _expect(not mismatch[&"ok"] and mismatch[&"errors"][-1][&"code"] == &"BALANCE_CACHE_MISMATCH", "reconciliation detects a damaged balance cache") _expect(economy.rebuild_balance_cache(), "balance cache can be rebuilt only from valid ledger entries") _expect(economy.get_balance(&"PLAYER_CASH") == 800, "rebuild never invents a compensating ledger entry") _expect(economy.validate_ledger()[&"ok"], "rebuilt balance cache reconciles exactly") func _test_snapshot_round_trip_is_atomic() -> void: var source := _economy() source.set_current_minute(777) source.initialize_account(&"PLAYER_CASH", 900, 360, &"init") source.transfer(&"PLAYER_CASH", &"GUEST_WALLET:guest_1", 120, 650, &"GIFT", &"player", &"gift") var snapshot := source.create_snapshot() var restored := EconomyServiceType.new() _expect(restored.restore_snapshot(snapshot), "valid economy snapshot restores") _expect(restored.create_snapshot() == snapshot, "economy snapshot round trip is exact") _expect(restored.validate_ledger()[&"ok"], "restored ledger and balances reconcile") var before_invalid := restored.create_snapshot() var corrupt := snapshot.duplicate(true) corrupt[&"entries"][0][&"amount"] = 0 _expect(not restored.restore_snapshot(corrupt), "snapshot containing a zero-amount entry is rejected") _expect(restored.create_snapshot() == before_invalid, "failed snapshot restore is fully atomic") var duplicate_account := snapshot.duplicate(true) duplicate_account[&"accounts"].append(duplicate_account[&"accounts"][0].duplicate(true)) _expect(not restored.restore_snapshot(duplicate_account), "snapshot with duplicate account definitions is rejected") _expect(restored.create_snapshot() == before_invalid, "second rejected restore also preserves live state") func _economy() -> EconomyService: return EconomyServiceType.new({ &"PLAYER_CASH": {&"allow_negative": false}, &"GUEST_WALLET:guest_1": {&"allow_negative": false}, &"FACILITY_REVENUE:inn": {&"allow_negative": false}, &"STAFF_WALLET:staff_1": {&"allow_negative": false}, &"STAFF_PAYABLE:staff_1": {&"allow_negative": true}, }) func _entry( absolute_minute: int, debit_account: StringName, credit_account: StringName, amount: int, category: StringName ) -> LedgerEntry: return LedgerEntryType.new().configure( &"", absolute_minute, debit_account, credit_account, amount, category ) func _expect(condition: bool, label: String) -> void: _checks += 1 if condition: print(" ✓ %s" % label) return _failures += 1 push_error(" ✗ %s" % label)