class_name EconomyService extends RefCounted ## Authoritative Gold ledger. There is intentionally no public balance setter: ## every change is derived from an append-only, balanced LedgerEntry. signal transaction_posted(transaction_id: StringName, entries: Array) const LedgerEntryType = preload("res://scripts/economy/ledger_entry.gd") const CommandResultType = preload("res://scripts/core/command_result.gd") const SNAPSHOT_VERSION := 1 const SYSTEM_SOURCE: StringName = &"SYSTEM_SOURCE" const SYSTEM_SINK: StringName = &"SYSTEM_SINK" const DEFAULT_CATEGORY: StringName = &"TRANSFER" var current_minute := 0 var _accounts: Dictionary = {} var _balance_cache: Dictionary = {} var _ledger: Array[LedgerEntry] = [] var _transaction_ids: Dictionary = {} var _idempotency_keys: Dictionary = {} var _next_transaction_sequence := 1 var _pending_transaction_notifications: Dictionary = {} # Transient nested observer boundary; never serialized as gameplay state. var _notification_scopes: Array[Array] = [] func _init(account_definitions: Dictionary = {}) -> void: # SOURCE may go negative because it is the explicit counter-account for # initialization/generation. SINK only receives destruction entries. _register_account_internal(SYSTEM_SOURCE, true, true) _register_account_internal(SYSTEM_SINK, false, true) var account_ids: Array = account_definitions.keys() account_ids.sort_custom(_variant_string_less) for account_value: Variant in account_ids: var account_id := StringName(account_value) if account_id in [SYSTEM_SOURCE, SYSTEM_SINK]: continue var definition: Variant = account_definitions[account_value] var allow_negative := false if definition is bool: allow_negative = definition elif definition is Dictionary: allow_negative = bool(definition.get( &"allow_negative", definition.get("allow_negative", false) )) _register_account_internal(account_id, allow_negative, false) func set_current_minute(absolute_minute: int) -> bool: if absolute_minute < 0: return false current_minute = absolute_minute return true func register_account(account_id: StringName, allow_negative := false) -> bool: if account_id.is_empty() or _accounts.has(account_id): return false return _register_account_internal(account_id, allow_negative, false) func has_account(account_id: StringName) -> bool: return _accounts.has(account_id) func account_allows_negative(account_id: StringName) -> bool: if not _accounts.has(account_id): return false return bool((_accounts[account_id] as Dictionary).get(&"allow_negative", false)) func get_account(account_id: StringName) -> Dictionary: if not _accounts.has(account_id): return {} var result: Dictionary = (_accounts[account_id] as Dictionary).duplicate(true) result[&"account_id"] = account_id result[&"balance"] = get_balance(account_id) return result func get_accounts() -> Array[Dictionary]: var result: Array[Dictionary] = [] for account_id: StringName in _sorted_string_name_keys(_accounts): result.append(get_account(account_id)) return result func get_balance(account_id: StringName) -> int: return int(_balance_cache.get(account_id, 0)) func initialize_account( account_id: StringName, amount: int, absolute_minute: int = 0, idempotency_key: StringName = &"", emit_notification := true ) -> CommandResult: if amount == 0: return _failure( CommandResultType.Code.VALIDATION_FAILED, "An initial balance must be non-zero." ) if amount > 0: return transfer( SYSTEM_SOURCE, account_id, amount, absolute_minute, &"INITIALIZATION", account_id, idempotency_key, &"account.initial_balance", &"", emit_notification ) return transfer( account_id, SYSTEM_SINK, -amount, absolute_minute, &"INITIALIZATION", account_id, idempotency_key, &"account.initial_balance", &"", emit_notification ) ## Moves Gold from from_account to to_account. The receiver is debited and the ## sender is credited, so signed balance is always debit minus credit. func transfer( from_account: StringName, to_account: StringName, amount: int, absolute_minute: int = -1, category: StringName = DEFAULT_CATEGORY, source_entity_id: StringName = &"", idempotency_key: StringName = &"", memo_key: StringName = &"", related_transaction_id: StringName = &"", emit_notification := true ) -> CommandResult: var minute := current_minute if absolute_minute < 0 else absolute_minute var entry := LedgerEntryType.new().configure( &"", minute, to_account, from_account, amount, category, source_entity_id, related_transaction_id, memo_key ) return post([entry], idempotency_key, &"", emit_notification) ## Convenience API for callers which naturally specify debit and credit. func post_entry( debit_account: StringName, credit_account: StringName, amount: int, absolute_minute: int, category: StringName, source_entity_id: StringName = &"", idempotency_key: StringName = &"", memo_key: StringName = &"", related_transaction_id: StringName = &"" ) -> CommandResult: var entry := LedgerEntryType.new().configure( &"", absolute_minute, debit_account, credit_account, amount, category, source_entity_id, related_transaction_id, memo_key ) return post([entry], idempotency_key) ## Appends one transaction. All entries are validated and all resulting account ## balances are checked before any live state is changed. func post( entries: Array, idempotency_key: StringName = &"", transaction_id: StringName = &"", emit_notification := true ) -> CommandResult: if entries.is_empty(): return _failure(CommandResultType.Code.INVALID_PAYLOAD, "A transaction needs at least one entry.") if not idempotency_key.is_empty() and _idempotency_keys.has(idempotency_key): return _failure( CommandResultType.Code.DUPLICATE_IDEMPOTENCY_KEY, "The idempotency key has already been posted.", {&"transaction_id": _idempotency_keys[idempotency_key]} ) var normalized: Array[LedgerEntry] = [] var draft_ids: Dictionary = {} for value: Variant in entries: var entry := _entry_from_value(value) if entry == null: return _failure(CommandResultType.Code.INVALID_PAYLOAD, "A ledger line is malformed.") if not entry.transaction_id.is_empty(): draft_ids[entry.transaction_id] = true normalized.append(entry) if draft_ids.size() > 1: return _failure( CommandResultType.Code.VALIDATION_FAILED, "All entries in one post must share a transaction ID." ) var generated := false var generated_sequence := _next_transaction_sequence var effective_id := transaction_id if effective_id.is_empty() and draft_ids.size() == 1: effective_id = draft_ids.keys()[0] if effective_id.is_empty(): var generated_data := _peek_generated_transaction_id() effective_id = generated_data[&"transaction_id"] generated_sequence = int(generated_data[&"sequence"]) generated = true if effective_id.is_empty() or _transaction_ids.has(effective_id): return _failure( CommandResultType.Code.DUPLICATE_COMMAND_ID, "The transaction ID is empty or already exists.", {&"transaction_id": effective_id} ) var candidate_balances := _balance_cache.duplicate(true) var debit_total := 0 var credit_total := 0 for entry: LedgerEntry in normalized: if not entry.transaction_id.is_empty() and entry.transaction_id != effective_id: return _failure( CommandResultType.Code.VALIDATION_FAILED, "A ledger line conflicts with the requested transaction ID." ) entry.transaction_id = effective_id var errors := entry.validation_errors() if not errors.is_empty(): return _failure( CommandResultType.Code.VALIDATION_FAILED, "A ledger line violates the ledger schema.", {&"errors": errors} ) if not _accounts.has(entry.debit_account) or not _accounts.has(entry.credit_account): return _failure( CommandResultType.Code.NOT_FOUND, "A ledger account is not registered.", { &"debit_account": entry.debit_account, &"credit_account": entry.credit_account, } ) if not entry.related_transaction_id.is_empty() \ and not _transaction_ids.has(entry.related_transaction_id): return _failure( CommandResultType.Code.NOT_FOUND, "The related transaction does not exist.", {&"related_transaction_id": entry.related_transaction_id} ) if not _system_account_use_is_valid(entry): return _failure( CommandResultType.Code.VALIDATION_FAILED, "SYSTEM_SOURCE and SYSTEM_SINK are restricted to explicit system flows.", {&"category": entry.category} ) candidate_balances[entry.debit_account] = int(candidate_balances[entry.debit_account]) + entry.amount candidate_balances[entry.credit_account] = int(candidate_balances[entry.credit_account]) - entry.amount debit_total += entry.amount credit_total += entry.amount if debit_total != credit_total: return _failure(CommandResultType.Code.VALIDATION_FAILED, "Debit and credit totals differ.") for account_id: StringName in _sorted_string_name_keys(candidate_balances): if int(candidate_balances[account_id]) < 0 and not account_allows_negative(account_id): return _failure( CommandResultType.Code.VALIDATION_FAILED, "An account does not permit a negative balance.", { &"account_id": account_id, &"available": get_balance(account_id), &"resulting_balance": int(candidate_balances[account_id]), } ) # Commit starts here. Every possibly failing check is complete. var start_index := _ledger.size() for entry: LedgerEntry in normalized: _ledger.append(entry.copy()) _balance_cache = candidate_balances _transaction_ids[effective_id] = { &"start_index": start_index, &"entry_count": normalized.size(), &"idempotency_key": idempotency_key, } if not idempotency_key.is_empty(): _idempotency_keys[idempotency_key] = effective_id if generated: _next_transaction_sequence = generated_sequence + 1 var posted_entries := get_transaction_entries(effective_id) if emit_notification: if _notification_scopes.is_empty(): transaction_posted.emit(effective_id, posted_entries) else: _notification_scopes.back().append(effective_id) else: _pending_transaction_notifications[effective_id] = true return CommandResultType.succeeded(0, { &"transaction_id": effective_id, &"entry_count": normalized.size(), &"entries": _entries_to_dictionaries(posted_entries), }) ## Retain ordinary post semantics while postponing observer delivery through a ## complete event effect/cost boundary. Explicit emit_notification=false remains ## owned by its existing caller, including Operations and supply transactions. func notification_scope_depth() -> int: return _notification_scopes.size() func begin_notification_scope() -> void: _notification_scopes.append([]) func end_notification_scope() -> void: assert(not _notification_scopes.is_empty(), "Balanced notification scope required.") if _notification_scopes.is_empty(): return var queued: Array = _notification_scopes.pop_back() if not _notification_scopes.is_empty(): _notification_scopes.back().append_array(queued) return var seen: Dictionary = {} for raw_id: Variant in queued: var transaction_id := StringName(raw_id) # Restoring a ledger removes rolled-back IDs. A failure branch can reuse # an ID; deliver the surviving final receipt once in insertion order. if seen.has(transaction_id) or not _transaction_ids.has(transaction_id): continue seen[transaction_id] = true transaction_posted.emit(transaction_id, get_transaction_entries(transaction_id)) func has_pending_transaction_notification(transaction_id: StringName) -> bool: return _pending_transaction_notifications.has(transaction_id) func commit_transaction_notification(transaction_id: StringName) -> bool: if not _pending_transaction_notifications.has(transaction_id) \ or not _transaction_ids.has(transaction_id): return false _pending_transaction_notifications.erase(transaction_id) transaction_posted.emit(transaction_id, get_transaction_entries(transaction_id)) return true func discard_transaction_notification(transaction_id: StringName) -> bool: if not _pending_transaction_notifications.has(transaction_id): return false _pending_transaction_notifications.erase(transaction_id) return true func reverse( transaction_id: StringName, absolute_minute: int = -1, idempotency_key: StringName = &"", category: StringName = &"REVERSAL", source_entity_id: StringName = &"", memo_key: StringName = &"transaction.reversal" ) -> CommandResult: var originals := get_transaction_entries(transaction_id) if originals.is_empty(): return _failure( CommandResultType.Code.NOT_FOUND, "The transaction to reverse was not found.", {&"transaction_id": transaction_id} ) if _has_related_financial_entry(transaction_id): return _failure( CommandResultType.Code.VALIDATION_FAILED, "The transaction has already been reversed or refunded.", {&"transaction_id": transaction_id} ) var minute := current_minute if absolute_minute < 0 else absolute_minute var entries: Array[LedgerEntry] = [] for original: LedgerEntry in originals: entries.append(original.reversed( &"", minute, category, source_entity_id, memo_key )) return post(entries, idempotency_key) func reverse_transaction( transaction_id: StringName, absolute_minute: int = -1, idempotency_key: StringName = &"" ) -> CommandResult: return reverse(transaction_id, absolute_minute, idempotency_key) ## Partial refunds are deliberately limited to a one-line source transaction; ## this avoids silently choosing an allocation across compound entries. func refund( transaction_id: StringName, amount: int, absolute_minute: int = -1, idempotency_key: StringName = &"", source_entity_id: StringName = &"", memo_key: StringName = &"transaction.refund" ) -> CommandResult: var originals := get_transaction_entries(transaction_id) if originals.is_empty(): return _failure(CommandResultType.Code.NOT_FOUND, "The transaction to refund was not found.") if originals.size() != 1: return _failure( CommandResultType.Code.VALIDATION_FAILED, "A partial refund needs a one-line source transaction." ) var original: LedgerEntry = originals[0] var refundable := original.amount - get_refunded_amount(transaction_id) if amount <= 0 or amount > refundable: return _failure( CommandResultType.Code.VALIDATION_FAILED, "The refund amount is outside the remaining refundable amount.", {&"refundable": refundable} ) var minute := current_minute if absolute_minute < 0 else absolute_minute var entry := original.reversed( &"", minute, &"REFUND", source_entity_id, memo_key ) entry.amount = amount return post([entry], idempotency_key) func refund_transaction( transaction_id: StringName, amount: int, absolute_minute: int = -1, idempotency_key: StringName = &"" ) -> CommandResult: return refund(transaction_id, amount, absolute_minute, idempotency_key) func get_refunded_amount(transaction_id: StringName) -> int: var originals := get_transaction_entries(transaction_id) if originals.size() != 1: return 0 var original: LedgerEntry = originals[0] var total := 0 for entry: LedgerEntry in _ledger: if entry.related_transaction_id == transaction_id \ and entry.debit_account == original.credit_account \ and entry.credit_account == original.debit_account: total += entry.amount return total func has_transaction(transaction_id: StringName) -> bool: return _transaction_ids.has(transaction_id) func get_transaction_id_for_idempotency_key(idempotency_key: StringName) -> StringName: return _idempotency_keys.get(idempotency_key, &"") func get_entries() -> Array[LedgerEntry]: var result: Array[LedgerEntry] = [] for entry: LedgerEntry in _ledger: result.append(entry.copy()) return result func get_transaction_entries(transaction_id: StringName) -> Array[LedgerEntry]: var result: Array[LedgerEntry] = [] if not _transaction_ids.has(transaction_id): return result var metadata: Dictionary = _transaction_ids[transaction_id] var start_index := int(metadata[&"start_index"]) var entry_count := int(metadata[&"entry_count"]) for index in range(start_index, start_index + entry_count): result.append(_ledger[index].copy()) return result func aggregate_categories(from_minute: int, to_minute_exclusive: int) -> Dictionary: var totals: Dictionary = {} for entry: LedgerEntry in _ledger: if entry.absolute_minute < from_minute or entry.absolute_minute >= to_minute_exclusive: continue totals[entry.category] = int(totals.get(entry.category, 0)) + entry.amount return _sorted_dictionary(totals) func aggregate_accounts(from_minute: int, to_minute_exclusive: int) -> Dictionary: var totals: Dictionary = {} for account_id: StringName in _sorted_string_name_keys(_accounts): totals[account_id] = {&"debit": 0, &"credit": 0, &"net": 0} for entry: LedgerEntry in _ledger: if entry.absolute_minute < from_minute or entry.absolute_minute >= to_minute_exclusive: continue var debit: Dictionary = totals[entry.debit_account] debit[&"debit"] = int(debit[&"debit"]) + entry.amount debit[&"net"] = int(debit[&"net"]) + entry.amount var credit: Dictionary = totals[entry.credit_account] credit[&"credit"] = int(credit[&"credit"]) + entry.amount credit[&"net"] = int(credit[&"net"]) - entry.amount return totals func get_daily_summary(day: int) -> Dictionary: if day < 1: return {} var start_minute := (day - 1) * 1_440 var end_minute := start_minute + 1_440 var transaction_set: Dictionary = {} var entry_count := 0 var total_debit := 0 var total_credit := 0 for entry: LedgerEntry in _ledger: if entry.absolute_minute < start_minute or entry.absolute_minute >= end_minute: continue entry_count += 1 transaction_set[entry.transaction_id] = true total_debit += entry.amount total_credit += entry.amount return { &"day": day, &"start_minute": start_minute, &"end_minute_exclusive": end_minute, &"category_totals": aggregate_categories(start_minute, end_minute), &"account_totals": aggregate_accounts(start_minute, end_minute), &"transaction_count": transaction_set.size(), &"entry_count": entry_count, &"total_debit": total_debit, &"total_credit": total_credit, &"balanced": total_debit == total_credit, } func daily_summary(day: int) -> Dictionary: return get_daily_summary(day) func validate_ledger() -> Dictionary: var calculated := _calculate_and_validate(_accounts, _ledger, _idempotency_keys) var errors: Array = calculated[&"errors"] var expected_balances: Dictionary = calculated[&"balances"] if expected_balances != _balance_cache: errors.append({ &"code": &"BALANCE_CACHE_MISMATCH", &"expected": expected_balances, &"actual": _balance_cache.duplicate(true), }) var expected_transactions: Dictionary = calculated[&"transactions"] if expected_transactions != _transaction_ids: errors.append({ &"code": &"TRANSACTION_INDEX_MISMATCH", &"expected": expected_transactions, &"actual": _transaction_ids.duplicate(true), }) return { &"ok": errors.is_empty(), &"errors": errors, &"total_debit": calculated[&"total_debit"], &"total_credit": calculated[&"total_credit"], &"account_count": _accounts.size(), &"transaction_count": _transaction_ids.size(), &"entry_count": _ledger.size(), } func validate() -> Dictionary: return validate_ledger() func rebuild_balance_cache() -> bool: var calculated := _calculate_and_validate(_accounts, _ledger, _idempotency_keys) if not (calculated[&"errors"] as Array).is_empty(): return false _balance_cache = (calculated[&"balances"] as Dictionary).duplicate(true) return true func create_snapshot() -> Dictionary: var saved_accounts: Array[Dictionary] = [] for account_id: StringName in _sorted_string_name_keys(_accounts): var definition: Dictionary = _accounts[account_id] saved_accounts.append({ &"account_id": String(account_id), &"allow_negative": bool(definition[&"allow_negative"]), &"system": bool(definition[&"system"]), }) var saved_entries: Array[Dictionary] = [] for entry: LedgerEntry in _ledger: saved_entries.append(entry.to_dictionary()) var saved_idempotency: Array[Dictionary] = [] for key: StringName in _sorted_string_name_keys(_idempotency_keys): saved_idempotency.append({ &"key": String(key), &"transaction_id": String(_idempotency_keys[key]), }) return { &"schema_version": SNAPSHOT_VERSION, &"current_minute": current_minute, &"next_transaction_sequence": _next_transaction_sequence, &"accounts": saved_accounts, &"entries": saved_entries, &"idempotency": saved_idempotency, } func restore_snapshot(snapshot: Dictionary) -> bool: if int(snapshot.get(&"schema_version", snapshot.get("schema_version", 0))) != SNAPSHOT_VERSION: return false var saved_accounts: Variant = snapshot.get(&"accounts", snapshot.get("accounts", null)) var saved_entries: Variant = snapshot.get(&"entries", snapshot.get("entries", null)) var saved_idempotency: Variant = snapshot.get(&"idempotency", snapshot.get("idempotency", null)) if not saved_accounts is Array or not saved_entries is Array or not saved_idempotency is Array: return false var restored_accounts: Dictionary = {} for value: Variant in saved_accounts: if not value is Dictionary: return false var account_id := StringName(value.get(&"account_id", value.get("account_id", ""))) if account_id.is_empty() or restored_accounts.has(account_id): return false restored_accounts[account_id] = { &"allow_negative": bool(value.get(&"allow_negative", value.get("allow_negative", false))), &"system": bool(value.get(&"system", value.get("system", false))), } if not restored_accounts.has(SYSTEM_SOURCE) or not restored_accounts.has(SYSTEM_SINK): return false if not bool((restored_accounts[SYSTEM_SOURCE] as Dictionary)[&"allow_negative"]) \ or bool((restored_accounts[SYSTEM_SINK] as Dictionary)[&"allow_negative"]): return false if not bool((restored_accounts[SYSTEM_SOURCE] as Dictionary)[&"system"]) \ or not bool((restored_accounts[SYSTEM_SINK] as Dictionary)[&"system"]): return false for account_id: Variant in restored_accounts: if account_id not in [SYSTEM_SOURCE, SYSTEM_SINK] \ and bool((restored_accounts[account_id] as Dictionary)[&"system"]): return false var restored_ledger: Array[LedgerEntry] = [] for value: Variant in saved_entries: if not value is Dictionary: return false restored_ledger.append(LedgerEntryType.from_dictionary(value)) var restored_idempotency: Dictionary = {} var idempotent_transactions: Dictionary = {} for value: Variant in saved_idempotency: if not value is Dictionary: return false var key := StringName(value.get(&"key", value.get("key", ""))) var transaction_id := StringName(value.get( &"transaction_id", value.get("transaction_id", "") )) if key.is_empty() or transaction_id.is_empty() \ or restored_idempotency.has(key) or idempotent_transactions.has(transaction_id): return false restored_idempotency[key] = transaction_id idempotent_transactions[transaction_id] = true var calculated := _calculate_and_validate( restored_accounts, restored_ledger, restored_idempotency ) if not (calculated[&"errors"] as Array).is_empty(): return false var restored_transactions: Dictionary = calculated[&"transactions"] var next_sequence := int(snapshot.get( &"next_transaction_sequence", snapshot.get("next_transaction_sequence", 0) )) var restored_minute := int(snapshot.get(&"current_minute", snapshot.get("current_minute", -1))) if next_sequence < 1 or restored_minute < 0: return false # Atomic swap: live fields are untouched until every saved value is valid. _accounts = restored_accounts _ledger = restored_ledger _idempotency_keys = restored_idempotency _transaction_ids = restored_transactions _balance_cache = (calculated[&"balances"] as Dictionary).duplicate(true) _next_transaction_sequence = next_sequence _pending_transaction_notifications.clear() current_minute = restored_minute return true func _register_account_internal( account_id: StringName, allow_negative: bool, is_system: bool ) -> bool: if account_id.is_empty() or _accounts.has(account_id): return false _accounts[account_id] = { &"allow_negative": allow_negative, &"system": is_system, } _balance_cache[account_id] = 0 return true func _entry_from_value(value: Variant) -> LedgerEntry: if value is LedgerEntry: return (value as LedgerEntry).copy() if value is Dictionary: return LedgerEntryType.from_dictionary(value) return null func _peek_generated_transaction_id() -> Dictionary: var sequence := maxi(_next_transaction_sequence, 1) var transaction_id := StringName("ledger_%08d" % sequence) while _transaction_ids.has(transaction_id): sequence += 1 transaction_id = StringName("ledger_%08d" % sequence) return {&"transaction_id": transaction_id, &"sequence": sequence} func _has_related_financial_entry(transaction_id: StringName) -> bool: for entry: LedgerEntry in _ledger: if entry.related_transaction_id == transaction_id: return true return false func _system_account_use_is_valid(entry: LedgerEntry) -> bool: if entry.debit_account not in [SYSTEM_SOURCE, SYSTEM_SINK] \ and entry.credit_account not in [SYSTEM_SOURCE, SYSTEM_SINK]: return true var normalized := String(entry.category).to_upper() return normalized.begins_with("INIT") \ or normalized in ["GENERATION", "DESTRUCTION", "REVERSAL", "REFUND", "MIGRATION"] func _calculate_and_validate( accounts: Dictionary, ledger: Array[LedgerEntry], idempotency: Dictionary ) -> Dictionary: var errors: Array[Dictionary] = [] var balances: Dictionary = {} for account_id: Variant in accounts: balances[StringName(account_id)] = 0 var transactions: Dictionary = {} var closed_transactions: Dictionary = {} var active_transaction: StringName = &"" var transaction_start := 0 var transaction_count := 0 var transaction_delta: Dictionary = {} var total_debit := 0 var total_credit := 0 for index in ledger.size(): var entry: LedgerEntry = ledger[index] var entry_errors := entry.validation_errors() if not entry_errors.is_empty(): errors.append({&"code": &"INVALID_ENTRY", &"index": index, &"errors": entry_errors}) continue if not accounts.has(entry.debit_account) or not accounts.has(entry.credit_account): errors.append({&"code": &"UNKNOWN_ACCOUNT", &"index": index}) continue if not _system_account_use_is_valid(entry): errors.append({&"code": &"INVALID_SYSTEM_ACCOUNT_USE", &"index": index}) if not entry.related_transaction_id.is_empty() \ and entry.related_transaction_id == entry.transaction_id: errors.append({&"code": &"SELF_RELATED_TRANSACTION", &"index": index}) if active_transaction != entry.transaction_id: if not active_transaction.is_empty(): _finalize_calculated_transaction( active_transaction, transaction_start, transaction_count, transaction_delta, accounts, balances, transactions, idempotency, errors ) closed_transactions[active_transaction] = true if closed_transactions.has(entry.transaction_id): errors.append({&"code": &"NON_CONTIGUOUS_TRANSACTION", &"index": index}) active_transaction = entry.transaction_id transaction_start = index transaction_count = 0 transaction_delta = {} transaction_count += 1 transaction_delta[entry.debit_account] = int(transaction_delta.get(entry.debit_account, 0)) + entry.amount transaction_delta[entry.credit_account] = int(transaction_delta.get(entry.credit_account, 0)) - entry.amount total_debit += entry.amount total_credit += entry.amount if not active_transaction.is_empty(): _finalize_calculated_transaction( active_transaction, transaction_start, transaction_count, transaction_delta, accounts, balances, transactions, idempotency, errors ) if total_debit != total_credit: errors.append({&"code": &"LEDGER_IMBALANCE"}) for entry: LedgerEntry in ledger: if not entry.related_transaction_id.is_empty() \ and not transactions.has(entry.related_transaction_id): errors.append({ &"code": &"MISSING_RELATED_TRANSACTION", &"transaction_id": entry.transaction_id, }) for key: Variant in idempotency: var related_id := StringName(idempotency[key]) if StringName(key).is_empty() or not transactions.has(related_id): errors.append({&"code": &"INVALID_IDEMPOTENCY_REFERENCE", &"key": key}) return { &"errors": errors, &"balances": balances, &"transactions": transactions, &"total_debit": total_debit, &"total_credit": total_credit, } func _finalize_calculated_transaction( transaction_id: StringName, start_index: int, entry_count: int, delta: Dictionary, accounts: Dictionary, balances: Dictionary, transactions: Dictionary, idempotency: Dictionary, errors: Array[Dictionary] ) -> void: if transactions.has(transaction_id): errors.append({&"code": &"DUPLICATE_TRANSACTION_ID", &"transaction_id": transaction_id}) return var candidate := balances.duplicate(true) for account_id: Variant in delta: candidate[account_id] = int(candidate.get(account_id, 0)) + int(delta[account_id]) for account_id: Variant in candidate: var definition: Dictionary = accounts.get(account_id, {}) if int(candidate[account_id]) < 0 and not bool(definition.get(&"allow_negative", false)): errors.append({ &"code": &"NEGATIVE_BALANCE_NOT_ALLOWED", &"transaction_id": transaction_id, &"account_id": account_id, }) return balances.clear() balances.merge(candidate, true) var idempotency_key: StringName = &"" for key: Variant in idempotency: if StringName(idempotency[key]) == transaction_id: idempotency_key = StringName(key) break transactions[transaction_id] = { &"start_index": start_index, &"entry_count": entry_count, &"idempotency_key": idempotency_key, } static func _entries_to_dictionaries(entries: Array[LedgerEntry]) -> Array[Dictionary]: var result: Array[Dictionary] = [] for entry: LedgerEntry in entries: result.append(entry.to_dictionary()) return result static func _sorted_string_name_keys(source: Dictionary) -> Array[StringName]: var result: Array[StringName] = [] for key: Variant in source: result.append(StringName(key)) result.sort_custom(func(left: StringName, right: StringName) -> bool: return String(left) < String(right) ) return result static func _sorted_dictionary(source: Dictionary) -> Dictionary: var result: Dictionary = {} for key: StringName in _sorted_string_name_keys(source): result[key] = source[key] return result static func _variant_string_less(left: Variant, right: Variant) -> bool: return String(left) < String(right) static func _failure( code: CommandResult.Code, message: String, details: Dictionary = {} ) -> CommandResult: return CommandResultType.failed(code, 0, message, details)