local accountLocks = {} local function money(value, allowZero) local amount = tonumber(value) if not amount then return nil end amount = math.floor(amount) if amount < (allowZero and 0 or 1) or amount > DevXConfig.MaxMoneyOperation then return nil end return amount end local function withLocks(accountIds, callback) local unique = {} for _, id in ipairs(accountIds) do id = tonumber(id) if id and not unique[id] then unique[id] = true end end for id in pairs(unique) do if accountLocks[id] then return false, 'Ein beteiligtes Konto verarbeitet gerade eine andere Buchung.' end end for id in pairs(unique) do accountLocks[id] = true end local ok, resultA, resultB = pcall(callback) for id in pairs(unique) do accountLocks[id] = nil end if not ok then print(('[devx_banking] Buchungsfehler: %s'):format(tostring(resultA))) return false, 'Interner Buchungsfehler.' end return resultA, resultB end local function getOverview(characterId) local account = MySQL.single.await([[ SELECT id, account_number AS accountNumber, account_name AS accountName, account_type AS accountType, balance FROM devx_bank_accounts WHERE character_id = ? AND account_type = 'personal' LIMIT 1 ]], { characterId }) if not account then return nil end account.id = tonumber(account.id) account.balance = tonumber(account.balance) or 0 local transactions = MySQL.query.await([[ SELECT id, direction, amount, balance_after AS balanceAfter, counterparty, reference, created_at AS createdAt FROM devx_bank_transactions WHERE account_id = ? ORDER BY id DESC LIMIT 25 ]], { account.id }) or {} for _, transaction in ipairs(transactions) do transaction.id = tonumber(transaction.id) transaction.amount = tonumber(transaction.amount) or 0 transaction.balanceAfter = tonumber(transaction.balanceAfter) or 0 end return { account = account, transactions = transactions } end local function addTransaction(accountId, direction, amount, balanceAfter, counterparty, reference) return MySQL.insert.await([[ INSERT INTO devx_bank_transactions (account_id, direction, amount, balance_after, counterparty, reference) VALUES (?, ?, ?, ?, ?, ?) ]], { accountId, direction, amount, balanceAfter, counterparty, reference }) end local function chargeCharacter(characterId, amount, reference, counterparty) amount = money(amount) if not amount then return false, 'Ungültiger Betrag.' end local account = MySQL.single.await([[ SELECT id, balance FROM devx_bank_accounts WHERE character_id = ? AND account_type = 'personal' LIMIT 1 ]], { characterId }) if not account then return false, 'Kein Bankkonto gefunden.' end return withLocks({ account.id }, function() account = MySQL.single.await('SELECT id, balance FROM devx_bank_accounts WHERE id = ? LIMIT 1', { account.id }) if not account or tonumber(account.balance) < amount then return false, 'Das Kontoguthaben reicht nicht aus.' end local newBalance = tonumber(account.balance) - amount local changed = MySQL.update.await('UPDATE devx_bank_accounts SET balance = ? WHERE id = ?', { newBalance, account.id }) if not changed or changed < 1 then return false, 'Belastung fehlgeschlagen.' end addTransaction(account.id, 'debit', amount, newBalance, counterparty or 'Unbekannt', reference or 'Zahlung') return true, newBalance end) end local function creditCharacter(characterId, amount, reference, counterparty) amount = money(amount) if not amount then return false, 'Ungültiger Betrag.' end local account = MySQL.single.await([[ SELECT id, balance FROM devx_bank_accounts WHERE character_id = ? AND account_type = 'personal' LIMIT 1 ]], { characterId }) if not account then return false, 'Kein Bankkonto gefunden.' end return withLocks({ account.id }, function() account = MySQL.single.await('SELECT id, balance FROM devx_bank_accounts WHERE id = ? LIMIT 1', { account.id }) if not account then return false, 'Kein Bankkonto gefunden.' end local newBalance = tonumber(account.balance) + amount local changed = MySQL.update.await('UPDATE devx_bank_accounts SET balance = ? WHERE id = ?', { newBalance, account.id }) if not changed or changed < 1 then return false, 'Gutschrift fehlgeschlagen.' end addTransaction(account.id, 'credit', amount, newBalance, counterparty or 'Unbekannt', reference or 'Gutschrift') return true, newBalance end) end local function setPlayerBalance(source, amount, reason) local player = exports.devx_core:GetPlayer(source) amount = money(amount, true) if not player or not player.accountId or amount == nil then return false, 'Ungültige Anfrage.' end return withLocks({ player.accountId }, function() local changed = MySQL.update.await('UPDATE devx_bank_accounts SET balance = ? WHERE id = ?', { amount, player.accountId }) if not changed or changed < 1 then return false, 'Kontostand konnte nicht geändert werden.' end addTransaction(player.accountId, 'credit', 0, amount, 'Administration', reason or 'Kontostand gesetzt') exports.devx_core:SetBankCache(source, amount) return true, amount end) end local function transferForSource(source, payload) local player = exports.devx_core:GetPlayer(source) local amount = money(payload.amount) local destinationNumber = type(payload.destination) == 'string' and payload.destination:gsub('%s+', ''):upper() or nil local reference = type(payload.reference) == 'string' and payload.reference:sub(1, 140) or 'Überweisung' if not player or not player.accountId then return false, 'Kein Charakter aktiv.' end if not amount or not destinationNumber or #destinationNumber < 4 then return false, 'Betrag oder Empfängerkonto ist ungültig.' end if destinationNumber == player.accountNumber then return false, 'Du kannst nicht auf dasselbe Konto überweisen.' end local destination = MySQL.single.await([[ SELECT id, account_number, account_name, balance FROM devx_bank_accounts WHERE account_number = ? LIMIT 1 ]], { destinationNumber }) if not destination then return false, 'Empfängerkonto wurde nicht gefunden.' end return withLocks({ player.accountId, destination.id }, function() local sourceAccount = MySQL.single.await([[ SELECT id, account_number, account_name, balance FROM devx_bank_accounts WHERE id = ? AND character_id = ? LIMIT 1 ]], { player.accountId, player.characterId }) destination = MySQL.single.await([[ SELECT id, account_number, account_name, balance FROM devx_bank_accounts WHERE id = ? LIMIT 1 ]], { destination.id }) if not sourceAccount or not destination then return false, 'Ein Konto wurde nicht gefunden.' end if tonumber(sourceAccount.balance) < amount then return false, 'Das Kontoguthaben reicht nicht aus.' end local sourceBalance = tonumber(sourceAccount.balance) - amount local destinationBalance = tonumber(destination.balance) + amount local transactionOk = MySQL.transaction.await({ { query = 'UPDATE devx_bank_accounts SET balance = ? WHERE id = ?', values = { sourceBalance, sourceAccount.id } }, { query = 'UPDATE devx_bank_accounts SET balance = ? WHERE id = ?', values = { destinationBalance, destination.id } }, { query = [[INSERT INTO devx_bank_transactions (account_id, direction, amount, balance_after, counterparty, reference) VALUES (?, 'debit', ?, ?, ?, ?)]], values = { sourceAccount.id, amount, sourceBalance, destination.account_name, reference } }, { query = [[INSERT INTO devx_bank_transactions (account_id, direction, amount, balance_after, counterparty, reference) VALUES (?, 'credit', ?, ?, ?, ?)]], values = { destination.id, amount, destinationBalance, sourceAccount.account_name, reference } } }) if not transactionOk then return false, 'Überweisung fehlgeschlagen.' end exports.devx_core:SetBankCache(source, sourceBalance) return true, sourceBalance end) end exports('GetOverview', getOverview) exports('ChargeCharacter', chargeCharacter) exports('CreditCharacter', creditCharacter) exports('SetPlayerBalance', setPlayerBalance) exports('TransferForSource', transferForSource) DevXRegisterCallback('banking:overview', function(source) local player = exports.devx_core:GetPlayer(source) if not player then return { ok = false, error = 'Kein Charakter aktiv.' } end local overview = getOverview(player.characterId) if not overview then return { ok = false, error = 'Kein Bankkonto gefunden.' } end exports.devx_core:SetBankCache(source, overview.account.balance) return { ok = true, overview = overview } end) DevXRegisterCallback('banking:transfer', function(source, payload) local success, result = transferForSource(source, payload) return success and { ok = true, balance = result } or { ok = false, error = result } end) RegisterCommand('setbank', function(source, args) if source ~= 0 and not IsPlayerAceAllowed(source, 'devx.admin') then return end local target = tonumber(args[1]) local amount = money(args[2], true) if not target or amount == nil then return end setPlayerBalance(target, amount, ('Admin %s'):format(source)) end, false)