class_name SupplyPurchaseQuoteV2 extends RefCounted ## Explicit total-order quote. v1 unit receipts are never reinterpreted. ## Placement uses the existing staff UI plan-first/legacy-live fallback rule. ## Total-order rounding reuses Pricing; attendance/role/floor multipliers are not added. const Pricing = preload("res://scripts/facility/facility_pricing.gd") const V1Plans = preload("res://scripts/supply/supply_auto_purchase_plans.gd") const PREFIX := "supply_purchase.quote.v2:" const MIMI := &"heroine_mimi" const MULTIPLIER_BP := 8000 const MAX_INT_TEXT := "9223372036854775807" const MAX_INT := 9223372036854775807 const KEYS: Array[StringName] = [&"version", &"item_id", &"quantity", &"base_unit_price", &"base_total", &"multiplier_bp", &"effective_total", &"applied_unique_id"] static func eligible(runtime: Variant) -> bool: var mimi: StaffRuntime = runtime.staff_service.get_staff(MIMI) if mimi == null or mimi.employment_kind != StaffRuntime.EmploymentKind.UNIQUE: return false var assignment: Dictionary = runtime.staff_service.get_work_assignment(MIMI) var facility_id := mimi.assigned_facility_id if not assignment.is_empty(): facility_id = StringName(assignment.get(&"facility_id", &"")) return not facility_id.is_empty() and runtime.facility_service.get_instance(facility_id) != null static func make(item_id: StringName, quantity: int, base_unit_price: int) -> Dictionary: @warning_ignore("integer_division") var maximum_base := (MAX_INT - 5000) / MULTIPLIER_BP if String(item_id).strip_edges().is_empty() or quantity <= 0 or quantity > 10000 or base_unit_price <= 0: return {} @warning_ignore("integer_division") var maximum_unit := maximum_base / quantity if base_unit_price > maximum_unit: return {} var base_total := quantity * base_unit_price return {&"version": 2, &"item_id": String(item_id), &"quantity": quantity, &"base_unit_price": base_unit_price, &"base_total": base_total, &"multiplier_bp": MULTIPLIER_BP, &"effective_total": Pricing.apply_multiplier(base_total, MULTIPLIER_BP), &"applied_unique_id": String(MIMI)} static func valid(quote: Dictionary) -> bool: if not keys_match(quote, KEYS): return false for key: StringName in [&"version", &"quantity", &"base_unit_price", &"base_total", &"multiplier_bp", &"effective_total"]: if typeof(quote[key]) != TYPE_INT: return false if typeof(quote[&"item_id"]) != TYPE_STRING or typeof(quote[&"applied_unique_id"]) != TYPE_STRING: return false if quote[&"version"] != 2 or quote[&"multiplier_bp"] != MULTIPLIER_BP \ or quote[&"applied_unique_id"] != String(MIMI): return false var canonical := make(StringName(quote[&"item_id"]), quote[&"quantity"], quote[&"base_unit_price"]) # This also preserves canonical key/container/type order without normalizing # corrupt saved fields or consulting today's catalog/personnel state. return not canonical.is_empty() and var_to_bytes(canonical) == var_to_bytes(quote) static func affordable_quantity(cash: int, space: int, base_unit_price: int) -> int: var lower := 0 var upper := mini(maxi(space, 0), 10000) while lower < upper: @warning_ignore("integer_division") var middle := (lower + upper + 1) / 2 var quote := make(&"affordability", middle, base_unit_price) if not quote.is_empty() and quote[&"effective_total"] <= cash: lower = middle else: upper = middle - 1 return lower static func receipt_memo(quote: Dictionary) -> StringName: if not valid(quote): return &"" var item: String = quote[&"item_id"] var actor: String = quote[&"applied_unique_id"] return StringName(PREFIX + "%d:%s:%d:%s:%d:%d:%d:%d:%d" % [item.length(), item, actor.length(), actor, quote[&"quantity"], quote[&"base_unit_price"], quote[&"base_total"], quote[&"multiplier_bp"], quote[&"effective_total"]]) static func decode_receipt_memo(memo: StringName) -> Dictionary: var text := String(memo) if not text.begins_with(PREFIX): return {} var item := _take_text(text.substr(PREFIX.length())) if item.is_empty(): return {} var actor := _take_text(item[&"rest"]) if actor.is_empty(): return {} var fields: PackedStringArray = String(actor[&"rest"]).split(":", true) if fields.size() != 5: return {} var quote := {&"version": 2, &"item_id": item[&"text"], &"quantity": _positive(fields[0]), &"base_unit_price": _positive(fields[1]), &"base_total": _positive(fields[2]), &"multiplier_bp": _positive(fields[3]), &"effective_total": _positive(fields[4]), &"applied_unique_id": actor[&"text"]} return quote if valid(quote) and receipt_memo(quote) == memo else {} static func receipt_from_ledger(economy: EconomyService, warehouse_id: StringName, item_id: StringName, minute: int, transaction_id: StringName, automatic := true, command_id := 0) -> Dictionary: var key := V1Plans.order_key(warehouse_id, item_id, minute) if automatic \ else StringName("supply.purchase.%d" % command_id) if economy == null or transaction_id.is_empty() or (not automatic and command_id <= 0) \ or economy.get_transaction_id_for_idempotency_key(key) != transaction_id: return {} var entries := economy.get_transaction_entries(transaction_id) if entries.size() != 1: return {} var entry: LedgerEntry = entries[0] var quote := decode_receipt_memo(entry.memo_key) if quote.is_empty() or quote[&"item_id"] != String(item_id) \ or entry.amount != quote[&"effective_total"] or entry.absolute_minute != minute \ or entry.source_entity_id != warehouse_id or entry.category != &"OPERATING_EXPENSE" \ or entry.credit_account != &"PLAYER_CASH" or entry.debit_account != &"OPERATING_EXPENSE" \ or not entry.related_transaction_id.is_empty(): return {} return quote static func has_history(runtime_data: Dictionary) -> bool: var economy: Variant = runtime_data.get(&"economy") if economy is Dictionary and economy.get(&"entries") is Array: for entry: Variant in economy[&"entries"]: if entry is Dictionary and typeof(entry.get(&"memo_key")) in [TYPE_STRING, TYPE_STRING_NAME] \ and String(entry[&"memo_key"]).begins_with(PREFIX): return true var plans: Variant = runtime_data.get(&"auto_purchase_plans") if plans is Dictionary: if plans.get(&"version") == 2: return true if plans.get(&"plans") is Array: for row: Variant in plans[&"plans"]: if row is Dictionary and row.has(&"last_quote"): return true var queue: Variant = runtime_data.get(&"command_queue") if queue is Dictionary and queue.get("processed") is Array: for result: Variant in queue["processed"]: if result is Dictionary and _result_has_quote(result): return true var stream: Variant = runtime_data.get(&"domain_events") if stream is Dictionary and stream.get("recent_events") is Array: for event: Variant in stream["recent_events"]: if event is Dictionary and event.get("event_type") in ["COMMAND_COMPLETED", "COMMAND_FAILED"] \ and event.get("payload") is Dictionary and _result_has_quote(event["payload"]): return true # The uncapped processed queue also keeps history after recent-event eviction. return false static func valid_history(runtime_data: Dictionary, economy: EconomyService) -> bool: if economy == null: return false var raw_economy: Variant = runtime_data.get(&"economy") if not raw_economy is Dictionary or not raw_economy.get(&"entries") is Array \ or not raw_economy.get(&"idempotency") is Array: return false var entries := economy.get_entries() if entries.size() != (raw_economy[&"entries"] as Array).size(): return false var v2_transactions: Dictionary = {} for index: int in entries.size(): var entry: LedgerEntry = entries[index] if not String(entry.memo_key).begins_with(PREFIX): continue # The old Economy parser permits legacy coercions. New v2 payment fields # must retain the complete actual nine-field producer representation. var raw: Variant = raw_economy[&"entries"][index] if not raw is Dictionary or var_to_bytes(raw) != var_to_bytes(entry.to_dictionary()): return false v2_transactions[String(entry.transaction_id)] = true for value: Variant in raw_economy[&"idempotency"]: if not value is Dictionary: return false if not v2_transactions.has(value.get(&"transaction_id")): continue if not keys_match(value, [&"key", &"transaction_id"]) \ or typeof(value[&"key"]) != TYPE_STRING or typeof(value[&"transaction_id"]) != TYPE_STRING \ or var_to_bytes(value) != var_to_bytes({&"key": value[&"key"], &"transaction_id": value[&"transaction_id"]}): return false var required_manual: Dictionary = {} for entry: LedgerEntry in economy.get_entries(): if not String(entry.memo_key).begins_with(PREFIX): continue var quote := decode_receipt_memo(entry.memo_key) if quote.is_empty() or entry.source_entity_id.is_empty(): return false var item := StringName(quote[&"item_id"]) var automatic := receipt_from_ledger(economy, entry.source_entity_id, item, entry.absolute_minute, entry.transaction_id) if not automatic.is_empty(): continue var command_id := _manual_command_id_for_transaction(economy, entry.transaction_id) if receipt_from_ledger(economy, entry.source_entity_id, item, entry.absolute_minute, entry.transaction_id, false, command_id).is_empty(): return false required_manual[command_id] = quote var queue: Variant = runtime_data.get(&"command_queue") if not queue is Dictionary or not queue.get("processed") is Array: return false var processed: Dictionary = {} for result: Variant in queue["processed"]: if not result is Dictionary or not _valid_result_quotes(result, economy): return false if typeof(result.get("command_id")) != TYPE_INT: return false processed[result["command_id"]] = result # Manual payments have uncapped processed results; removing/replacing their # frozen quote is invalid. Auto plans may legitimately be replaced/pruned, # so their append-only ledger receipt remains sufficient historical proof. for command_id: int in required_manual: if not processed.has(command_id): return false var result: Dictionary = processed[command_id] if result.get("code") != "OK" or not result.get("details") is Dictionary \ or not result["details"].get(&"purchase_quote") is Dictionary \ or var_to_bytes(result["details"][&"purchase_quote"]) != var_to_bytes(required_manual[command_id]): return false var stream: Variant = runtime_data.get(&"domain_events") if not stream is Dictionary or not stream.get("recent_events") is Array: return false for event: Variant in stream["recent_events"]: if not event is Dictionary: return false if event.get("event_type") not in ["COMMAND_COMPLETED", "COMMAND_FAILED"]: continue var payload: Variant = event.get("payload") if not payload is Dictionary or not _result_has_quote(payload): continue var command_id: Variant = event.get("causation_command_id") if typeof(command_id) != TYPE_INT or not processed.has(command_id) \ or typeof(event.get("source_service")) != TYPE_STRING or event["source_service"] != "simulation_runtime" \ or var_to_bytes(payload) != var_to_bytes(processed[command_id]): return false return true static func _result_has_quote(result: Dictionary) -> bool: # Only the owned original_result chain is traversed; arbitrary dictionaries # and schema-looking strings elsewhere are not version authorities. var current: Dictionary = result var seen: Array[Dictionary] = [] while true: for visited: Dictionary in seen: if is_same(visited, current): return false seen.append(current) var details: Variant = current.get("details") if not details is Dictionary: return false if details.has(&"purchase_quote"): return true var original: Variant = details.get("original_result") if not original is Dictionary: return false current = original return false static func _valid_result_quotes(result: Dictionary, economy: EconomyService) -> bool: var current: Dictionary = result var seen: Array[Dictionary] = [] while true: for visited: Dictionary in seen: if is_same(visited, current): return false seen.append(current) var details: Variant = current.get("details") if not details is Dictionary: return false if details.has(&"purchase_quote"): # Existing CommandQueue owns all seven result fields; quote results # additionally retain their exact producer string/container types. if not _canonical_result(current): return false var quote: Variant = details[&"purchase_quote"] if not quote is Dictionary or not valid(quote): return false if current["code"] == "OK": if typeof(details.get(&"quantity")) != TYPE_INT or details[&"quantity"] != quote[&"quantity"] \ or typeof(details.get(&"total_cost")) != TYPE_INT or details[&"total_cost"] != quote[&"effective_total"] \ or typeof(details.get(&"warehouse_id")) != TYPE_STRING_NAME or typeof(details.get(&"item_id")) != TYPE_STRING_NAME \ or typeof(details.get(&"transaction_id")) != TYPE_STRING_NAME: return false var entries := economy.get_transaction_entries(details[&"transaction_id"]) if entries.size() != 1: return false var receipt := receipt_from_ledger(economy, details[&"warehouse_id"], details[&"item_id"], (entries[0] as LedgerEntry).absolute_minute, details[&"transaction_id"], false, current["command_id"]) if var_to_bytes(receipt) != var_to_bytes(quote): return false var original: Variant = details.get("original_result") if not original is Dictionary: return true current = original return false static func _canonical_result(result: Dictionary) -> bool: if result.size() != 7: return false for key: Variant in result: if typeof(key) != TYPE_STRING or key not in ["code", "command_id", "target_id", "message", "resolution_hints", "details", "is_replay"]: return false if typeof(result["code"]) != TYPE_STRING or String(result["code"]) not in CommandResult.Code.keys() \ or typeof(result["command_id"]) != TYPE_INT or result["command_id"] <= 0 \ or typeof(result["target_id"]) != TYPE_STRING or typeof(result["message"]) != TYPE_STRING \ or not result["resolution_hints"] is Array or not result["details"] is Dictionary \ or typeof(result["is_replay"]) != TYPE_BOOL: return false for hint: Variant in result["resolution_hints"]: if typeof(hint) != TYPE_STRING: return false return true static func _manual_command_id_for_transaction(economy: EconomyService, transaction_id: StringName) -> int: for row: Dictionary in economy.create_snapshot()[&"idempotency"]: var key: String = row[&"key"] if row[&"transaction_id"] != String(transaction_id) or not key.begins_with("supply.purchase."): continue var number := _positive(key.substr("supply.purchase.".length())) if key == "supply.purchase.%d" % number: return number return 0 static func keys_match(value: Dictionary, expected: Array[StringName]) -> bool: if value.size() != expected.size(): return false for key: Variant in value: if typeof(key) != TYPE_STRING_NAME or key not in expected: return false return true static func _take_text(text: String) -> Dictionary: var delimiter := text.find(":") if delimiter <= 0: return {} var length := _positive(text.substr(0, delimiter)) var rest := text.substr(delimiter + 1) if length <= 0 or length >= rest.length() or rest.substr(length, 1) != ":": return {} return {&"text": rest.substr(0, length), &"rest": rest.substr(length + 1)} static func _positive(text: String) -> int: if text.is_empty() or text.length() > MAX_INT_TEXT.length() or text.substr(0, 1) == "0": return 0 for index: int in text.length(): var code := text.unicode_at(index) if code < 48 or code > 57: return 0 if text.length() == MAX_INT_TEXT.length() and text > MAX_INT_TEXT: return 0 return text.to_int()